diff --git a/frontend/apps/artcraft-website/src/components/generation-gallery/GenerationGalleryGrid.tsx b/frontend/apps/artcraft-website/src/components/generation-gallery/GenerationGalleryGrid.tsx
index f7204f906e..2185df5388 100644
--- a/frontend/apps/artcraft-website/src/components/generation-gallery/GenerationGalleryGrid.tsx
+++ b/frontend/apps/artcraft-website/src/components/generation-gallery/GenerationGalleryGrid.tsx
@@ -144,6 +144,7 @@ export function GenerationGalleryGrid({
modelLabel={entry.job.modelLabel}
progress={entry.job.progress}
estimatedTimeLeftMs={entry.job.estimatedTimeLeftMs}
+ batchCount={entry.job.batchCount}
/>
)}
{entry.kind === "failed" && (
diff --git a/frontend/apps/artcraft-website/src/components/generation-gallery/PendingCard.tsx b/frontend/apps/artcraft-website/src/components/generation-gallery/PendingCard.tsx
index 660f794511..c6222e3d96 100644
--- a/frontend/apps/artcraft-website/src/components/generation-gallery/PendingCard.tsx
+++ b/frontend/apps/artcraft-website/src/components/generation-gallery/PendingCard.tsx
@@ -10,6 +10,7 @@ export interface PendingCardProps {
prompt: string;
progress?: number;
estimatedTimeLeftMs?: number;
+ batchCount?: number;
}
const formatTimeLeft = (ms: number): string => {
@@ -29,6 +30,7 @@ export const PendingCard = memo(function PendingCard({
prompt,
progress,
estimatedTimeLeftMs,
+ batchCount,
}: PendingCardProps) {
const progressPercent =
progress != null ? Math.max(0, Math.min(100, Math.round(progress))) : null;
@@ -44,6 +46,11 @@ export const PendingCard = memo(function PendingCard({
return (
+ {batchCount != null && batchCount > 1 && (
+
+ Generating {batchCount} videos · Results may appear one at a time
+
+ )}
) => void;
setPendingRecreate: (payload: RecreatePayload | null) => void;
consumePendingRecreate: () => RecreatePayload | null;
- startBatch: (prompt: string, modelLabel: string) => string;
+ startBatch: (prompt: string, modelLabel: string, batchCount?: number) => string;
setBatchJobToken: (batchId: string, jobToken: string) => void;
completeBatch: (batchId: string, video: GeneratedVideo) => void;
failBatch: (batchId: string, reason?: string) => void;
@@ -99,7 +100,7 @@ export const useCreateVideoStore = create((set, get) => ({
return payload;
},
- startBatch: (prompt, modelLabel) => {
+ startBatch: (prompt, modelLabel, batchCount) => {
const id = crypto.randomUUID();
const batch: VideoBatch = {
id,
@@ -107,6 +108,7 @@ export const useCreateVideoStore = create((set, get) => ({
status: "pending",
createdAt: Date.now(),
modelLabel,
+ batchCount,
};
set((s) => ({ batches: [...s.batches, batch] }));
return id;
diff --git a/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
index 3cf707e7af..1e4ee9c125 100644
--- a/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
+++ b/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
@@ -275,6 +275,7 @@ export default function CreateVideo() {
}, [charactersLoaded, storeSetCharacters, storeSetLoaded]);
// Batch store (enqueue flow only)
+ const batches = useCreateVideoStore((s) => s.batches);
const startBatch = useCreateVideoStore((s) => s.startBatch);
const setBatchJobToken = useCreateVideoStore((s) => s.setBatchJobToken);
const completeBatch = useCreateVideoStore((s) => s.completeBatch);
@@ -310,6 +311,26 @@ export default function CreateVideo() {
excludeUploads: true,
});
+ // Map job token → batch count so PendingCard can show "N videos generating"
+ const jobTokenToBatchCount = useMemo(() => {
+ const map = new Map();
+ for (const batch of batches) {
+ if (batch.jobToken && batch.batchCount && batch.batchCount > 1) {
+ map.set(batch.jobToken, batch.batchCount);
+ }
+ }
+ return map;
+ }, [batches]);
+
+ const enrichedInProgress = useMemo(
+ () =>
+ jobs.inProgress.map((job) => {
+ const batchCount = jobTokenToBatchCount.get(job.id);
+ return batchCount ? { ...job, batchCount } : job;
+ }),
+ [jobs.inProgress, jobTokenToBatchCount],
+ );
+
const newlyCompletedTokens = useMemo(
() => new Set(jobs.newlyCompleted.map((i) => i.id)),
[jobs.newlyCompleted],
@@ -690,8 +711,20 @@ export default function CreateVideo() {
needsImage ||
!selectedModel
) {
+ console.log("[generate-video] blocked", {
+ hasPrompt: !!prompt.trim(),
+ isGenerating: isGeneratingRef.current,
+ needsImage,
+ hasModel: !!selectedModel,
+ });
return;
}
+ console.log("[generate-video] starting", {
+ model: selectedModel.model,
+ numVideos,
+ inputMode,
+ isReferenceMode,
+ });
isGeneratingRef.current = true;
setIsGenerating(true);
@@ -743,7 +776,7 @@ export default function CreateVideo() {
const baseParams = {
prompt: prompt.trim(),
model: selectedModel.model,
- numVideos: 1,
+ numVideos,
aspectRatio: selectedSize,
duration: duration ?? selectedModel.duration_seconds_default ?? undefined,
resolution: hasResolutionOptions
@@ -767,45 +800,53 @@ export default function CreateVideo() {
: undefined,
referenceCharacterTokens,
};
+ console.log("[generate-video] params", baseParams);
- // Enqueue each video as a separate job so they complete independently
const modelLabel = selectedModel.full_name ?? selectedModel.model;
- const count = Math.max(1, numVideos);
+ const batchId = startBatch(prompt, modelLabel, numVideos > 1 ? numVideos : undefined);
- for (let i = 0; i < count; i++) {
- const batchId = startBatch(prompt, modelLabel);
-
- try {
- const result = await enqueueVideoGeneration(baseParams);
-
- if (!result.success || !result.jobToken) {
- failBatch(batchId, result.error ?? "Failed to start generation");
- continue;
- }
+ try {
+ console.log("[generate-video] enqueueing job...");
+ const result = await enqueueVideoGeneration(baseParams);
+ console.log("[generate-video] enqueue result", result);
+ if (!result.success || !result.jobToken) {
+ console.warn("[generate-video] enqueue failed", result.error);
+ failBatch(batchId, result.error ?? "Failed to start generation");
+ } else {
setBatchJobToken(batchId, result.jobToken);
+ console.log("[generate-video] polling started", {
+ jobToken: result.jobToken,
+ });
const stopPolling = startVideoPolling(
result.jobToken,
(video) => {
+ console.log("[generate-video] complete", {
+ batchId,
+ media_token: video.media_token,
+ });
completeBatch(batchId, video);
pollingCleanupsRef.current.delete(batchId);
window.dispatchEvent(new Event("task-queue-update"));
},
(reason) => {
+ console.warn("[generate-video] poll failed", { batchId, reason });
failBatch(batchId, reason);
pollingCleanupsRef.current.delete(batchId);
window.dispatchEvent(new Event("task-queue-update"));
},
);
pollingCleanupsRef.current.set(batchId, stopPolling);
- } catch {
- failBatch(batchId, "Network error - please try again");
}
+ } catch (err) {
+ console.error("[generate-video] unexpected error", err);
+ failBatch(batchId, "Network error - please try again");
}
window.dispatchEvent(new Event("credits-change"));
window.dispatchEvent(new Event("task-queue-update"));
+ console.log("[generate-video] done enqueuing");
setIsGenerating(false);
isGeneratingRef.current = false;
}, [
@@ -861,7 +902,7 @@ export default function CreateVideo() {
glowOrbs={videoGlowOrbs}
gridContent={