Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
3055143cf1
  1. 52
      src/components/nodes/VideoStitchNode.tsx
  2. 1
      src/store/utils/nodeDefaults.ts
  3. 21
      src/store/workflowStore.ts
  4. 1
      src/types/nodes.ts

52
src/components/nodes/VideoStitchNode.tsx

@ -431,16 +431,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
{renderHandles()}
<div className="flex-1 flex flex-col min-h-0 gap-2">
{/* Filmstrip UI */}
<div className="flex-1 flex flex-col min-h-0 gap-2">
{/* Filmstrip + controls area (shrink-0: only takes space it needs) */}
<div className="shrink-0 flex flex-col gap-2">
{orderedClips.length === 0 ? (
<div className="flex-1 flex items-center justify-center border border-dashed border-neutral-600 rounded">
<div className="h-16 flex items-center justify-center border border-dashed border-neutral-600 rounded">
<span className="text-[10px] text-neutral-500">Connect videos to stitch</span>
</div>
) : (
<>
{/* Filmstrip */}
<div className="flex-1 overflow-y-auto nowheel grid grid-cols-4 content-start gap-2 p-2 bg-neutral-900/50 rounded">
<div className="overflow-y-auto nowheel grid grid-cols-4 content-start gap-2 p-2 bg-neutral-900/50 rounded">
{orderedClips.map((clip) => {
const thumbnail = thumbnails.get(clip.edgeId);
return (
@ -511,14 +511,6 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
})}
</div>
{/* Stitch button */}
<button
onClick={handleStitch}
disabled={orderedClips.length < 2 || nodeData.status === "loading" || isRunning}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-neutral-700 disabled:text-neutral-500 disabled:cursor-not-allowed rounded text-white text-xs font-medium transition-colors"
>
{nodeData.status === "loading" ? "Processing..." : "Stitch"}
</button>
</>
)}
</div>
@ -549,16 +541,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
</div>
)}
{/* Output preview */}
{/* Output preview (flex-1: grows with node) */}
{nodeData.outputVideo && nodeData.status !== "loading" && (
<div className="relative w-full">
<div className="relative flex-1 min-h-0">
<video
src={nodeData.outputVideo}
controls
autoPlay
loop
muted
className="w-full h-auto max-h-40 object-contain rounded"
className="w-full h-full object-contain rounded"
playsInline
/>
<button
@ -572,6 +564,36 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
</button>
</div>
)}
{/* Controls row: Loop selector + Stitch button (below video, right-aligned) */}
{orderedClips.length > 0 && (
<div className="shrink-0 flex items-center justify-end gap-2">
<div className="flex items-center gap-1">
<span className="text-[10px] text-neutral-400">Loop</span>
{([1, 2, 3] as const).map((count) => (
<button
key={count}
onClick={() => updateNodeData(id, { loopCount: count })}
className={`nodrag px-1.5 py-0.5 rounded text-[10px] font-medium transition-colors ${
(nodeData.loopCount || 1) === count
? "bg-blue-600 text-white"
: "bg-neutral-700 text-neutral-400 hover:bg-neutral-600 hover:text-neutral-300"
}`}
>
{count}x
</button>
))}
</div>
<button
onClick={handleStitch}
disabled={orderedClips.length < 2 || nodeData.status === "loading" || isRunning}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-neutral-700 disabled:text-neutral-500 disabled:cursor-not-allowed rounded text-white text-xs font-medium transition-colors"
>
{nodeData.status === "loading" ? "Processing..." : "Stitch"}
</button>
</div>
)}
</div>
</BaseNode>
);

1
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,

21
src/store/workflowStore.ts

@ -2036,6 +2036,15 @@ export const useWorkflowStore = create<WorkflowStore>((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<WorkflowStore>((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<WorkflowStore>((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<WorkflowStore>((set, get) => ({
const { stitchVideosAsync } = await import('@/hooks/useStitchVideos');
const outputBlob = await stitchVideosAsync(
videoBlobs,
loopedBlobs,
audioData,
(progress) => {
updateNodeData(nodeId, { progress: progress.progress });

1
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

Loading…
Cancel
Save