Media Workflow Status
Canonical status and verification snapshot for the media workflow suite.
This page covers the demo workflows that ship with Graphyn and the node types they depend on:
io.resolve_pathio.file_readmedia.video_importmedia.audio_extractmedia.audios_listmedia.audio_mixmedia.videos_listmedia.video_stitchmedia.video_encodemedia.text_to_speechmedia.caption_stylemedia.file_outputpreview.view(generic value preview, used to surface audio handles)graphyn.sticky_note(on-canvas guide annotation embedded in each template)
Phase 2 (captioning & composition) adds:
media.image_import(loads an image handle + dimensions; produces the handle OCR consumes)media.caption_overlay(burns timed captions into a video)media.video_compose(layers overlay clips over a base video)media.timing_controller(averages sync points into delays)media.speech_to_text(audio → text + timed caption segments)media.ocr(image → text + bounding blocks)media.video_overlay+media.overlays_list(build the overlay listvideo_composeneeds)media.sync_point+media.sync_points_list(build the sync-point listtiming_controllerneeds)media.audio_encode(saves an audio handle to WAV/MP3/AAC so audio templates can output a file)
Phase 3 (image ops) adds:
media.image_resize+media.image_crop(scale / trim an image)media.images_list+media.image_sequence_to_video(render images into an MP4 slideshow)
Current Status
Phase 1 media workflows are implemented for JVM/Desktop and are covered by both template contract tests and workflow execution tests.
Phase 2 nodes are implemented for JVM/Desktop and covered by plugin unit tests (with fakes) plus
availability-guarded FFmpeg backend tests. Every Phase 2 capability now ships in a template — four flows:
Captioned Video (speech_to_text → caption_overlay + caption_style), Document Text Extract
(image_import → ocr), Picture-in-Picture (video_overlay → overlays_list → video_compose),
and Sync Calibration (sync_point → sync_points_list → timing_controller). The builder +
collector nodes assemble the record lists video_compose and timing_controller consume.
Node Status
| Node | Area | Implementation | Direct unit test | Template wiring test | Workflow execution test | Notes |
|---|---|---|---|---|---|---|
io.resolve_path |
shared IO helper | implemented | yes (IoPluginTest) |
yes | yes | Resolves demo resource paths before file reads |
io.file_read |
shared IO helper | implemented | yes (IoPluginTest) |
yes | yes | Reads the text fixtures used by the demo templates |
media.video_import |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | FFprobe-backed video metadata import |
media.audio_extract |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | Extracts the first audio stream to WAV |
media.audios_list |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | Collector helper used before media.audio_mix |
media.audio_mix |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | FFmpeg-backed mix with optional volumes |
media.videos_list |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | Collector helper used before media.video_stitch |
media.video_stitch |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | Cut-only concat flow in Phase 1 |
media.video_encode |
media-core |
implemented | yes (MediaCorePluginTest) |
yes | yes | MP4 render with destination output_path input |
media.audio_encode |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (TTS, Audio Mix) | yes | Saves audio to WAV/MP3/AAC; lets audio templates terminate in media.file_output |
media.text_to_speech |
media-ai |
implemented | yes (MediaAiPluginTest) |
yes | yes | Cache key includes text, language, voice, and speed |
media.caption_style |
media-ai |
implemented | yes (MediaAiPluginTest) |
yes | yes | Metadata-only node for Phase 2 caption overlays |
media.file_output |
preview | implemented | no dedicated direct unit test yet | yes | yes | Pass-through file preview; only video-encode terminals expose a file_path |
preview.view |
preview | implemented | yes (PreviewPluginTest) |
yes | yes | Generic opaque preview; used to surface TTS/mix audio handles |
graphyn.sticky_note |
sticky-notes | implemented | n/a (annotation) | yes | yes | No-op executor so an embedded guide note never fails execution |
media.image_import |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Document Text Extract) | yes | FFprobe-backed image handle + dimensions; produces the image handle media.ocr consumes |
media.caption_overlay |
media-core |
implemented | yes (MediaCorePluginTest, FfmpegMediaCoreBackendTest) |
yes (Captioned Video) | yes | Burns captions via the ass filter; requires FFmpeg built with libass |
media.video_compose |
media-core |
implemented | yes (MediaCorePluginTest, FfmpegMediaCoreBackendTest) |
yes (Picture-in-Picture) | yes | Overlay-filter chain with per-overlay timing + opacity |
media.timing_controller |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Sync Calibration) | yes | Pure compute; averages (source_ms,target_ms) sync points into delays |
media.video_overlay |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Picture-in-Picture) | yes | Builds one overlay record for video_compose |
media.overlays_list |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Picture-in-Picture) | yes | Collects overlay records into the compose list |
media.sync_point |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Sync Calibration) | yes | Builds one (source_ms,target_ms) record |
media.sync_points_list |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Sync Calibration) | yes | Collects sync-point records into the timing list |
media.speech_to_text |
media-ai |
implemented | yes (MediaAiPluginTest) |
yes (Captioned Video) | yes | CLI adapter GRAPHYN_STT_EXECUTABLE; emits caption segments |
media.ocr |
media-ai |
implemented | yes (MediaAiPluginTest) |
yes (Document Text Extract) | yes | CLI adapter GRAPHYN_OCR_EXECUTABLE; pairs with media.image_import |
media.image_resize |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Image Edit) | yes | FFmpeg scale; outputs a resized image handle |
media.image_crop |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Image Edit) | yes | FFmpeg crop; trims to an x/y/w/h region |
media.images_list |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Slideshow) | yes | Collects image handles for the sequence encoder |
media.image_sequence_to_video |
media-core |
implemented | yes (MediaCorePluginTest) |
yes (Slideshow) | yes | Concat-demuxer slideshow at a fixed fps |
Template Coverage
| Template | Status | Covered by | Notes |
|---|---|---|---|
| Simple Text to Speech | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Path resolution, file read, TTS → audio_encode (WAV) → media.file_output |
| Video Narration | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Import, audio extraction, narration, mixing, encoding. Output preview via media.file_output |
| Audio Mix | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Video → extract + TTS → mix → audio_encode (MP3) → media.file_output; caption-style metadata |
| Smart Video Encode | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Script-driven bitrate selection before encode. Output preview via media.file_output |
| Video Stitch | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Clip ordering, stitching, encode. Output preview via media.file_output |
| Captioned Video | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 2: transcribe → style → burn-in captions → encode. Output via media.file_output |
| Document Text Extract | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 2: import image → OCR → preview text. Needs GRAPHYN_OCR_EXECUTABLE to run |
| Picture-in-Picture | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 2: build overlay → compose over base → encode. Needs FFmpeg overlay filter |
| Sync Calibration | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 2: build sync points → average into delays → preview config. Pure compute |
| Image Edit | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 3: import → resize → crop → preview |
| Slideshow | ready | MediaWorkflowTemplateTest, MediaWorkflowExecutionTest |
Phase 3: import images → images_list → sequence → encode → output |
The launcher groups templates by WorkflowCategory (Media / Data & IO / Examples); media templates
are the Media section. Every template also carries a graphyn.sticky_note guide node (title, flow, use-cases, tips) and
ends in an output-preview node. Templates ship without positions; the editor runs auto-layout on
first load (see Editor Behavior).
Editor Behavior
- Auto-layout on load. Demo templates have no stored node positions, so the editor dispatches
AutoLayoutonce the canvas is measured (GraphynApp), guarded so a saved/edited layout is never clobbered. Triggered for all scenes, not just one. - Annotation parking.
performAutoLayoutlays out the dataflow DAG, then parks annotation nodes (sticky guides) in a column to the left of the graph so they read as a legend. Annotation positions/sizes are included in the layout result so the viewport fit frames them too. - Default card. Nodes without a registered
NodeCanvasFactory(e.g.io.resolve_path) render through a defaultFieldCardFactorysized from their spec — there is no separate fallback card. - Output preview rule.
media.file_outputrequires afile_path(String); onlymedia.video_encodeterminals expose one. Audio/TTS terminals output an opaque audio handle, so those templates preview withpreview.viewinstead.
Verification Commands
Run the focused media checks:
./gradlew :app:app:jvmTest --tests com.ronjunevaldoz.graphyn.bootstrap.MediaWorkflowTemplateTest
./gradlew :app:app:jvmTest --tests com.ronjunevaldoz.graphyn.bootstrap.MediaWorkflowExecutionTest
./gradlew :plugins:media-core:test :plugins:media-ai:test
Run the full demo JVM suite:
./gradlew :app:app:jvmTest
Known Gaps / Missing
media.file_outputstill lacks a dedicated direct unit test file.- The workflow-level execution tests use deterministic fakes (no FFmpeg/TTS launch). Real-tool
coverage is per-node and availability-guarded:
FfmpegMediaCoreBackendTestand thesay/tesseract fallback tests. There is no single end-to-end test that runs a whole template against real tools. - Speech-to-text has no zero-config fallback (no standard CLI), so the captioning template still
needs
GRAPHYN_STT_EXECUTABLE. TTS (macOSsay) and OCR (tesseract) do fall back automatically. media.video_stitchsupports only thecuttransition in Phase 1.media.video_composeoverlays are video handles only; image and text overlays are deferred.media.image_importreads only dimensions (no color space / frame extraction yet).- Slideshow assumes input frames share dimensions (no auto-scale in
image_sequence_to_video). - Remaining Phase 3 nodes (
audio_resample, advanced/custom encoding) are still planned inmedia-workflow-plan.md.
Known Bugs / Constraints
- System dependencies required for real runs.
media-coreshells out to FFmpeg/FFprobe andmedia-aito the TTS/STT/OCR binaries (GRAPHYN_TTS_EXECUTABLE,GRAPHYN_STT_EXECUTABLE,GRAPHYN_OCR_EXECUTABLE). If these are absent, real execution fails at the node; the deterministic tests still pass because they fake the executors/engines. media.caption_overlayneeds FFmpeg built with libass. It burns subtitles via theassfilter. On a stripped FFmpeg the executor fails fast with a clear message (FfmpegMediaCoreBackend.supportsFilter("ass")), and the backend test skips that leg.- Working directory sensitivity.
media.video_encode'soutput_pathand resolved input paths are interpreted relative to the process working directory. The desktop app runs fromapp/desktopApp/, which is why templates resolve inputs viaio.resolve_pathagainst../../app/app/src/commonMain/resources/media. Generated outputs (*.mp4,*.wav) land in the desktop app dir and are git-ignored. - Script string escaping. Kotlin Script (
script.eval) code embedded in a triple-quoted workflow definition must escape template variables as$$nameso the host compiler does not interpolate them (seeScriptSpecKDoc). JVM-only APIs such asString.formatare fine — the script runs on the JVM. media.*types are JVM-only. The media plugins live insrc/main(JVM). In the KMPcommonTestregistry they are unregistered, so scene validation ignoresunknown_node_typefor media scenes and lists media types injvmOnlyTypes.
Conflicts & Gotchas
- A node in a
WorkflowDefinitionalways needs an executor or execution throws"No executor registered for node type '…'". Annotation nodes are an editor concept the core engine does not know about, sographyn.sticky_noteregisters a no-op executor to stay harmless inside an executable template. media.file_outputandpreview.viewcards: the spec/executor arecommonMain, but the JVMmedia.file_outputcard (Open button, file metadata) isjvmMain-only.- Adding nodes to a template breaks the exact-match assertions in
MediaWorkflowTemplateTestand the node-count assertions inMediaWorkflowExecutionTest— update both together.
Do's and Don'ts
Do
- Resolve every file path through io.resolve_path (base dir + relative) instead of hardcoding.
- Terminate a video template in media.video_encode → media.file_output; terminate an audio/TTS
template in preview.view.
- Give each template one graphyn.sticky_note guide (title, flow, use-cases, tips).
- Keep templates position-free and rely on auto-layout-on-load.
- Update MediaWorkflowTemplateTest and MediaWorkflowExecutionTest whenever a template changes.
Don't
- Don't wire an audio handle into media.file_output (it expects a file_path String).
- Don't hardcode absolute paths or assume the project root as the working directory.
- Don't add domain-specific nodes to the sample plugins (see CLAUDE.md).
- Don't bake node positions into a template — let auto-layout place them.
- Don't commit generated run outputs (app/desktopApp/*.mp4 / *.wav); they are git-ignored.