From 3055143cf14316c1cf7b83e35177b0166d83b7dd Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 8 Feb 2026 00:24:22 +1300 Subject: [PATCH] fix: apply loopCount in videoStitch regenerateNode path The loopCount (1x/2x/3x) feature was only applied in executeWorkflow() but not in regenerateNode(), which is the code path triggered by clicking the "Stitch" button. This adds the blob duplication logic to both code paths and includes the UI controls and type definitions. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/VideoStitchNode.tsx | 52 +++++++++++++++++------- src/store/utils/nodeDefaults.ts | 1 + src/store/workflowStore.ts | 21 +++++++++- src/types/nodes.ts | 1 + 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/components/nodes/VideoStitchNode.tsx b/src/components/nodes/VideoStitchNode.tsx index 2f3ef73d..9ae54545 100644 --- a/src/components/nodes/VideoStitchNode.tsx +++ b/src/components/nodes/VideoStitchNode.tsx @@ -431,16 +431,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps - {/* Filmstrip UI */} -
+ {/* Filmstrip + controls area (shrink-0: only takes space it needs) */} +
{orderedClips.length === 0 ? ( -
+
Connect videos to stitch
) : ( <> {/* Filmstrip */} -
+
{orderedClips.map((clip) => { const thumbnail = thumbnails.get(clip.edgeId); return ( @@ -511,14 +511,6 @@ export function VideoStitchNode({ id, data, selected }: NodeProps - {/* Stitch button */} - )}
@@ -549,16 +541,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps )} - {/* Output preview */} + {/* Output preview (flex-1: grows with node) */} {nodeData.outputVideo && nodeData.status !== "loading" && ( -
+
)} + + {/* Controls row: Loop selector + Stitch button (below video, right-aligned) */} + {orderedClips.length > 0 && ( +
+
+ Loop + {([1, 2, 3] as const).map((count) => ( + + ))} +
+ + +
+ )}
); diff --git a/src/store/utils/nodeDefaults.ts b/src/store/utils/nodeDefaults.ts index 96133b82..f5657549 100644 --- a/src/store/utils/nodeDefaults.ts +++ b/src/store/utils/nodeDefaults.ts @@ -195,6 +195,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => { clips: [], clipOrder: [], outputVideo: null, + loopCount: 1, status: "idle", error: null, progress: 0, diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index f7acc5b0..a8db1854 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -2036,6 +2036,15 @@ export const useWorkflowStore = create((set, get) => ({ inputs.videos.map((v) => fetch(v).then((r) => r.blob())) ); + // Duplicate blobs based on loopCount (2x or 3x repeats the sequence) + // Each copy must be a new Blob so BlobSource can read it independently + const loopCount = nodeData.loopCount || 1; + const loopedBlobs = loopCount > 1 + ? Array.from({ length: loopCount }, () => + videoBlobs.map((b) => new Blob([b], { type: b.type })) + ).flat() + : videoBlobs; + // Prepare audio if connected let audioData = null; if (inputs.audio.length > 0 && inputs.audio[0]) { @@ -2053,7 +2062,7 @@ export const useWorkflowStore = create((set, get) => ({ // Stitch videos const { stitchVideosAsync } = await import('@/hooks/useStitchVideos'); const outputBlob = await stitchVideosAsync( - videoBlobs, + loopedBlobs, audioData, (progress) => { updateNodeData(node.id, { progress: progress.progress }); @@ -2962,6 +2971,14 @@ export const useWorkflowStore = create((set, get) => ({ inputs.videos.map((v) => fetch(v).then((r) => r.blob())) ); + // Duplicate blobs based on loopCount (2x or 3x repeats the sequence) + const loopCount = nodeData.loopCount || 1; + const loopedBlobs = loopCount > 1 + ? Array.from({ length: loopCount }, () => + videoBlobs.map((b) => new Blob([b], { type: b.type })) + ).flat() + : videoBlobs; + let audioData = null; if (inputs.audio.length > 0 && inputs.audio[0]) { const { prepareAudioAsync } = await import('@/hooks/useAudioMixing'); @@ -2976,7 +2993,7 @@ export const useWorkflowStore = create((set, get) => ({ const { stitchVideosAsync } = await import('@/hooks/useStitchVideos'); const outputBlob = await stitchVideosAsync( - videoBlobs, + loopedBlobs, audioData, (progress) => { updateNodeData(nodeId, { progress: progress.progress }); diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 9cf5b111..cf02c5b9 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -233,6 +233,7 @@ export interface VideoStitchNodeData extends BaseNodeData { clips: VideoStitchClip[]; // Ordered clip sequence for filmstrip clipOrder: string[]; // Edge IDs in user-defined order (drag reorder) outputVideo: string | null; // Stitched video blob URL or data URL + loopCount: 1 | 2 | 3; // How many times to repeat the clip sequence (1 = no loop) status: NodeStatus; error: string | null; progress: number; // 0-100 processing progress