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={