Pipeline Architecture
How OpenReels transforms a topic into a rendered video through a 6-stage Mastra workflow.
OpenReels uses a linear 6-stage pipeline built on Mastra workflows. Each stage is a createStep that receives the output of the previous stage, with shared state passed through closures for non-serializable data like provider instances and callbacks.
The 6 stages
Topic
|
v
Research ---- web search grounds the script in facts
|
v
Director ---- writes the DirectorScore, then Critic evaluates it
| ^ (revision loop: up to 2 rounds until score >= 7)
| |
v
TTS --------- generates voiceover audio with word-level timestamps
|
v
Visuals ----- resolves images, video clips, stock footage, and music (parallel)
|
v
Assembly ---- Remotion bundles and renders the final MP4
|
v
Critic ------ scores the output; flags revision if below thresholdEach stage reports progress through PipelineCallbacks -- the same interface used by both the CLI (terminal spinners) and the web UI (Server-Sent Events).
The DirectorScore
The central data structure that flows through the pipeline is the DirectorScore. It is produced by the Creative Director agent in stage 2 and consumed by every downstream stage.
interface DirectorScore {
emotional_arc: string; // e.g. "curiosity-to-wisdom"
archetype: string; // e.g. "cinematic_documentary"
music_mood: MusicMood; // one of 8 mood tags
scenes: Scene[]; // 3-16 scenes
}
interface Scene {
visual_type: "ai_image" | "ai_video" | "stock_image" | "stock_video" | "text_card";
visual_prompt: string; // image gen prompt or stock search query
motion: "zoom_in" | "zoom_out" | "pan_right" | "pan_left" | "static";
script_line: string; // voiceover narration for this scene
transition: TransitionType | null; // how this scene flows into the next
}The DirectorScore is saved as score.json in the output directory. It drives TTS script generation, visual asset resolution, Remotion composition props, and critic evaluation.
Data flow between stages
Stages pass data through two mechanisms:
- Mastra's input/output chaining -- each step's
outputSchemafeeds the next step'sinputSchemavia.then(). - Shared closures -- non-serializable data (provider instances, accumulated LLM usage, the score itself) lives in objects scoped to
buildPipelineWorkflowand accessed by steps via closure.
researchStep ─(ResearchResult)─> directorStep ─(done)─> ttsStep ─(done)─> visualsStep ─(done)─> assemblyStep ─(done)─> criticStepThe Research step outputs a ResearchResult with summary, key facts, mood, and sources. From the Director step onward, each step reads the shared directorResult, ttsResult, and visualsResult objects that accumulate state as the pipeline progresses.
Stage lifecycle
Every stage follows the same pattern:
- Call
cb.onStageStart(stageName)to signal the UI - Execute the core work (LLM call, TTS generation, rendering, etc.)
- Push any LLM token usage to the shared
llmUsagesarray for cost tracking - Call
cb.onStageComplete(stageName, detail, durationSec)with a human-readable summary - Push a log entry to
log.stagesfor the run log
If a stage fails non-fatally (research web search fails, critic evaluation errors), it calls cb.onStageSkip and continues with graceful defaults. Fatal errors propagate up and terminate the pipeline.
Cost estimation
After the Director stage produces the DirectorScore, the pipeline computes an estimated cost based on the number of AI images, stock scenes, TTS characters, video clips, and music generation. In interactive mode (CLI without --yes), the user is prompted to confirm before spending money on asset generation.
After all stages complete, actual LLM token usage is aggregated and the real cost is computed and reported.
Score replay
If you already have a score.json from a previous run, you can replay it with --score <path> (CLI) or the score field (API). This skips three stages:
score.json (loaded)
|
v
Director ---- populates closure bindings, runs cost estimation (no LLM calls)
|
v
TTS --------- generates voiceover from the replayed score's script lines
|
v
Visuals ----- resolves assets and music
|
v
Assembly ---- renders the final videoResearch and Critic are skipped entirely. The Director step still runs to populate the shared state that downstream stages depend on (archetype config, cost breakdown), but it uses the provided score directly instead of calling the LLM. Cost estimates during replay accurately omit the skipped LLM calls.
This is useful when a previous run produced a great creative plan but downstream stages failed (wrong audio duration, image generation error, etc.). Replay saves $0.50-2.00 and 10-30 seconds per run.
Creative direction
The --direction <file> flag lets you attach a free-form markdown brief that influences the DirectorScore generation. The AI reads it like a creative brief from a producer, honoring your constraints on visual style, mood, script notes, and scene ideas while still exercising creative judgment on anything not specified.
Direction text is injected into both the initial generation and revision prompts, so the critic/revise loop stays aligned with your intent. See examples/direction-brief.md for a sample.
Early exits
The pipeline supports two early exit paths:
- Dry run (
--dry-run) -- stops after the Director stage (including the quality gate revision loop) and prints the final DirectorScore JSON. No assets are generated. Works with both--directionand--score. - Cost rejection -- if the user declines at the cost estimate prompt, the pipeline exits cleanly.
Both paths still write log.json to the output directory.
Output directory
Each run creates a timestamped directory under the output path:
output/
2025-01-15-143022-fall-of-the-roman-empire/
score.json # DirectorScore
log.json # stage timings, cost, resolution metadata
final.mp4 # rendered video
assets/
scene-0-ai.png
scene-1-stock.jpg
scene-2-ai-video.mp4
voiceover.mp3Source files
| File | Role |
|---|---|
src/pipeline/orchestrator.ts | Mastra workflow builder and runPipeline entry point |
src/pipeline/utils.ts | PipelineCallbacks, PipelineOptions, stage name constants, word splitting |
src/pipeline/music-resolver.ts | Music resolution with Lyria/bundled fallback |