- Output Node now automatically triggers execution when a new connection is made
- Added Run button to Output Node header for manual triggering
- Updated regenerateNode to support output node type
- Prevents re-triggering on initial mount or disconnections using edge count tracking
Resolves issue where Output Node would not display data from already-generated upstream nodes unless connected before generation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove Save As button and handler from Header (feature not ready)
- Remove Array node button from FloatingActionBar (not ready for release)
- Add error logging for failed Kie.ai tasks to aid debugging
- Add .superset/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The thumbnail extraction effect had three issues compounding on weak
hardware:
1. The onseeked promise had no timeout — if seek hangs on systems
without hardware video decoding, the entire effect blocks
indefinitely. Added 10-second Promise.race timeout.
2. Created video elements were never cleaned up after use, leaving
phantom decoders running. Added cleanupVideo() that nulls handlers,
clears src, and calls load().
3. The thumbnail cache was cleared wholesale on every run
(thumbnailsRef.current = new Map()), making the cache check on
the next line always miss. Replaced with fingerprint-based
invalidation: only clips whose video data actually changed get
re-extracted, reusing cached thumbnails for unchanged clips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Data URLs force Chrome to re-parse base64 on each access. With autoPlay
loop, this happens continuously and can freeze the main thread on systems
with weak GPUs that fall back to software video decoding.
Add useVideoBlobUrl hook that converts data URLs to blob URLs via
fetch->blob->createObjectURL. Returns the data URL immediately as
fallback to avoid flicker, then swaps to blob URL once ready (~50ms).
Properly revokes blob URLs on input change and unmount.
Applied to all 6 <video> elements across 5 components:
- GenerateVideoNode (outputVideo)
- VideoStitchNode (outputVideo)
- OutputNode (contentSrc, inline + lightbox)
- EaseCurveNode (outputVideo)
- VideoTrimNode (previewUrl)
Store data is unaffected — getConnectedInputs, executors, and
imageStorage all read outputVideo from the Zustand store, not from
the rendering layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getVideoDimensions created a <video> element for dimension detection but
never cleaned it up, leaving a phantom decoder running alongside the
visible video. On systems with weak GPUs (e.g. Intel HD 5500), this
compounds with autoPlay loop to overwhelm Chrome's main thread.
- Add cleanup helper that nulls handlers, clears src, and calls load()
- Add video.preload = "metadata" to avoid fetching full video content
- Add 10-second timeout that cleans up and resolves null if metadata
never loads (graceful degradation: node keeps current size)
- Apply same cleanup pattern to getImageDimensions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix nanoBananaExecutor narrowing issue where promptText was narrowed
to `never` after type guard, and fix addEdge type mismatch where
Edge<T> data can be undefined but addEdge expects it defined.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Save the previous workflowId and workflowName before mutating state.
If saveToFile() fails, restore the previous values so the workflow
identity is unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of mutating selectedOutputIndex N times in a loop (which races
with React renders), pass { arrayItemIndex: index } as an edge data
override through onConnect. Adds optional edgeDataOverrides parameter
to onConnect and addEdgeWithType.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reject patterns with nested quantifiers (e.g. `(a+)+`) that cause
catastrophic backtracking, cap regex input at 100K chars, and add
lime-400 minimap color for array nodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The clipKey dependency used a presence-only check ("1"/"0"), so when a
video re-generated (truthy→truthy), the effect never re-ran. Additionally,
the ref-based cache keyed by edgeId returned stale thumbnails even if the
effect did fire. Fix by fingerprinting video data via its trailing 20 chars
and clearing the thumbnail cache when clipKey changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The end slider's full-width invisible body (higher z-index) was intercepting
all pointer events, preventing the start slider from receiving input. Fix by
setting pointer-events: none on both range inputs and pointer-events: all on
only the thumb pseudo-elements. Swap z-index so the start handle is on top
when both thumbs overlap at position 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When importing a workflow via file picker, loadWorkflow had no way to
find the image files on disk because directoryPath was only stored in
localStorage (which doesn't survive browser switches or clears).
Now auto-save embeds directoryPath in the workflow JSON, and
loadWorkflow uses it as a fallback after the workflowPath parameter
and localStorage config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saved workflows can contain multiple edges with the same ID (from
repeated connections or migration normalization). React warns about
duplicate keys and may render edges incorrectly. Deduplicate by
keeping the last occurrence of each edge ID on load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saved workflows could contain edges targeting nanoBanana nodes with
indexed handle IDs (image-0, text-0) from a previous schema-based
handle format. GenerateImageNode now always renders non-indexed
handles (image, text), causing React Flow error #008 on every
interaction. Normalize these edge handles during loadWorkflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Audio from generateAudio was being silently dropped during video
stitching. Add [VideoStitch] checkpoint logs at each stage where
audio can be discarded: getConnectedInputs, prepareAudioAsync,
and codec detection. Surface codec failure as a visible progress
warning instead of only console.warn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old preview model ID (gemini-2.5-flash-preview-image-generation) was
deprecated. Updated to the current stable ID across provider, cost
calculator, tests, and docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add path traversal guard to workflow-images GET handler (matching POST handler)
- Preserve stored prompt in generateAudioExecutor when connected text is null
- Reset conflicting isVideo/isAudio flags in kie.ts content-type detection
- Fix Infinity aspect ratio causing zero-height canvas in VideoStitchNode
- Check response content-type before trusting isAudioModel in fal.ts
- Move audio feeding inside inner try block in useStitchVideos for proper cleanup
- Add 30s seek timeout to frame extraction in videoProcessingExecutors
- Surface file-open errors via toast in Generate3DNode instead of failing silently
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared video-probing module (constants, helpers, probeVideoMetadata)
from useTrimVideo and useStitchVideos into src/lib/video-probing.ts
- Fix audio interleaving in useTrimVideo to feed audio before video frames,
matching useStitchVideos fix for Discord playback compatibility
- Fix blob URL leaks in VideoTrimNode and videoProcessingExecutors
- Fix nanoBananaExecutor: null coercion for non-string prompts, redact PII
in console.warn, remove duplicate dynamicInputs.prompt
- Fix generateAudioExecutor double updateNodeData on error paths
- Fix fal.ts media fetch ordering for 3D model detection
- Fix kie.ts and replicate.ts content-type detection for audio models
- Fix WorkflowCanvas connection drop menu wiring for generateAudio nodes
- Fix open-file route Windows path quoting
- Add path traversal protection to workflow-images route
- Add AudioInputNode onerror handler for metadata loading failures
- Fix useAudioPlayback uncaught play() promise rejection
- Add generateAudio case to regenerateNode in workflowStore
- Update placeholder Discord links to real invite URL
- Extract ProviderBadge into shared component in Generate3DNode
- Fix VideoTrimNode duration detection with proper cleanup and cancellation
- Fix GenerateAudioNode test fetch mock cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore fal-only cost tracking guard in video/audio executors — other
providers don't have reliable pricing. Also add generateAudio to
hasNonGeminiProviders so the CostIndicator hides when audio nodes
are present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this, videoTrim nodes were silently skipped during full
workflow execution (Cmd+Enter). The regenerateNode and
executeSelectedNodes paths already had it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add generateAudio and videoFrameGrab cases to executeNode dispatcher
- Add selectedModel null guards in route.ts for all non-Gemini providers
- Fix ConnectionDropMenu wrap-around test (5->6 items after generateAudio)
- Fix audio duration edge cases (NaN guard in formatTime, truthiness check)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch AVC_LEVEL_4_0 from Baseline profile (avc1.420028) to High profile
(avc1.640028) for broader platform compatibility. Feed audio to the muxer
before video frames so packets are properly interleaved — previously all
audio was dumped after video, causing Discord to skip the audio stream.
Probe total video duration upfront to trim audio to match video length.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register VideoFrameGrabNode in node exports, nodeTypes map, and minimap colors
- Add to ConnectionDropMenu as video target (Frame Grab) and image source
- Add executeVideoFrameGrab executor with canvas-based frame extraction
- Wire into all three execution paths: executeWorkflow, regenerateNode, executeSelectedNodes
- Add video connection validation for videoFrameGrab target type
- Add handle mapping for video->videoFrameGrab and videoFrameGrab->image connections
- Add videoFrameGrab case to quickstart validation defaults
- Video input handle (left, purple) and image output handle (right, blue)
- First/Last frame position toggle with output clearing on toggle change
- Extract Frame button triggers regenerateNode
- Image preview area with clear button overlay
- Processing spinner overlay and error display
- BaseNode wrapper with title, comment, and keyboard run support
- Add 'videoFrameGrab' to NodeType union and VideoFrameGrabNodeData interface
- Register default dimensions (320x320) and default data in nodeDefaults
- Wire getSourceOutput to return image type from videoFrameGrab.outputImage
- Add videoFrameGrab dimensions to local Record types in WorkflowCanvas and validation
1. Add "videoTrim" to source type check in orderedClips so VideoTrim
node outputs are recognized and thumbnails are extracted
2. Generate thumbnails at source video's native aspect ratio instead
of fixed 160x120 (4:3) to prevent distortion
3. Change thumbnail display from object-cover to object-contain to
prevent cropping/distortion when the node is resized
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes in pasteNodes:
1. Deep copy node data with JSON.parse/stringify (was shallow spread)
2. requestAnimationFrame selection correction to override React Flow's
stale selection reconciliation that could re-select original nodes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The VideoTrim executor was missing from three places:
- execution/index.ts barrel export
- workflowStore.ts regenerateNode if-else chain
- workflowStore.ts executeSelectedNodes switch
This caused the Trim button to complete instantly with no processing
and no video output downstream.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Export VideoTrimNode from src/components/nodes/index.ts
- Register videoTrim in nodeTypes map in WorkflowCanvas.tsx
- Add videoTrim to getNodeHandles (video in, video out)
- Add videoTrim to isValidConnection video target list
- Add videoTrim connection drop handling (video input and output)
- Add videoTrim minimap color: blue-400 (#60a5fa)
- Add VideoTrim to VIDEO_TARGET_OPTIONS in ConnectionDropMenu (scissors icon)
- Add VideoTrim to VIDEO_SOURCE_OPTIONS in ConnectionDropMenu (scissors icon)
- Dual range sliders with visual timeline scrubber for start/end times
- Inline video preview with play/pause controls (shows source or trimmed)
- Source/Trimmed toggle when output is available
- Time labels: start, duration, end (M:SS.s format)
- Trim button (disabled when no source, loading, or invalid range)
- Processing overlay with progress percentage
- Error display for processing failures
- Encoder check states (not supported / checking)
- Auto-detects video duration on connect and sets endTime
- Add 'videoTrim' to NodeType union and VideoTrimNodeData interface in nodes.ts
- Add VideoTrimNodeData to WorkflowNodeData union
- Add default dimensions (360x360) and default data to nodeDefaults.ts
- Create useTrimVideo.ts hook with MediaBunny frame-by-frame trim + audio preservation
- Add getSourceOutput case for videoTrim in connectedInputs.ts
- Add executeVideoTrim executor to videoProcessingExecutors.ts
- Wire 'videoTrim' case into executeNode.ts dispatcher
- Fix duplicate Record<NodeType> dimensions in WorkflowCanvas.tsx and validation.ts
- Resolve prompt from dynamicInputs.prompt in Gemini code path as fallback
- Return clear 400 error when prompt is provided but is not a string (corrupted data)
- Improve parallel batch error logging to include nodeId and nodeType for diagnosis
- Replace find() with for-loop to preserve index mapping between results and batch nodes
- Add defensive type coercion guard for promptText (handles non-string at runtime)
- Sanitize dynamicInputs.prompt to string before including in request payload
- Add final assertion guard before fetch to provide clear error if prompt is not a string
- Ensure text values in connectedInputs are always strings via String() coercion