Mortiflix · project overview

A motion design studio on your own machine

Claude makes the video one stage at a time (brief, script, style frames, animatic, final) and you approve every stage. This page covers the whole project on one page: what it does, how one video moves through it, every module, and UML diagrams of the structure and the behaviour.

~4.6klines of JavaScript (src + web)
1runtime dependency (@anthropic-ai/sdk)
3backends: Claude Code, API, demo
3pipelines: explainer, short, logo sting
25tests, all passing in ~2 s
AGPL-3.0license, Node ≥ 20
Overview

The idea

Most AI video tools take one prompt and return one result. Mortiflix works like a real studio instead: the work moves through stages, and a Claude session stops at every gate. It submits what it made, writes a handoff and ends. When you respond (approve, pin notes on a frame or a moment, answer questions), a fresh session picks up from the journal. Waiting on you costs nothing, for hours or for days.

Three ideas carry the design:

01
The rules live in code, not in the prompt. Only you can approve a reviewed step. A submission is refused unless it reports every error check and answers every note on the last version. What you reviewed is copied out of the session's reach.
02
It learns your studio. When you point out a real mistake, the session proposes a new error check. Approve it once and every future video runs it. Your taste carries across projects in TASTE.md.
03
Pipelines are folders. A pipeline.json (steps, review modes, checks), a PIPELINE.md (the craft) and skills. Anyone can write one.
Use cases

Who does what

One person owns the studio: they write briefs and review every stage. Sessions (Claude) do the work and can only reach the studio through mfx. The studio process enforces the rules between them.

flowchart LR
  owner(["👤 Owner"])
  claude(["🤖 Claude session"])
  subgraph studio ["Mortiflix studio"]
    direction TB
    uc1(["Write a brief / start a project"])
    uc2(["Review a step: approve or ask for changes"])
    uc3(["Pin notes on frames, moments, paragraphs"])
    uc4(["Answer questions"])
    uc5(["Approve proposed error checks"])
    uc6(["Pause · resume · cancel"])
    uc7(["Do the step's work"])
    uc8(["Report status and progress"])
    uc9(["Submit at a gate (with checks)"])
    uc10(["Ask a question · propose a check"])
    uc11(["Queue a render"])
    uc12(["Hand off and stop"])
  end
  owner --- uc1 & uc2 & uc3 & uc4 & uc5 & uc6
  claude --- uc7 & uc8 & uc9 & uc10 & uc11 & uc12
        

Diagrams need the Mermaid library from cdn.jsdelivr.net (open this page online).

UML · use caseThe owner's actions only exist in the web API and the CLI; the session's only exist through mfx.
Structure

Components

One Node process (mortiflix serve or mortiflix run) built around one folder, the studio. Two front doors for you (the CLI and the web studio) and one for the session (mfx over a private Unix socket). All of them end in gates.mjs, the only code that changes a project's record.

flowchart TB
  subgraph you ["For you"]
    cli["cli.mjs
bin/mortiflix"] web["web/server.mjs
+ web/app.js"] end subgraph sess ["For the session"] mfx["mfx.mjs
bin/mfx"] bridge["bridge.mjs
Unix socket, per-session token"] end subgraph core ["Core"] gates["gates.mjs
every rule"] projects["projects.mjs
record, events, journal"] pipelines["pipelines.mjs
load · validate · snapshot"] studio["studio.mjs
paths, config, secrets, locks"] end subgraph run ["Running sessions"] runner["runner.mjs
Runner"] torch["torch.mjs
workdir + CLAUDE.md"] renderq["renderq.mjs
RenderQueue"] subgraph backends ["backends/"] cc["claude-code"] api["anthropic-api"] demo["demo"] end end voice["voice/
index · elevenlabs · qwen"] cli & web --> gates cli & web --> runner cli & web --> voice mfx -- "JSON over socket" --> bridge bridge --> gates bridge --> renderq runner --> torch runner --> bridge runner --> renderq runner --> backends runner -- "settle()" --> gates demo -. "drives, like a session" .-> mfx gates --> projects --> pipelines
UML · componentArrows are the main calls; every module reads paths, config and locks through studio.mjs (not drawn). The demo backend drives the real bridge through mfx, exactly like a session would.
Runtime

Deployment

Everything runs on one machine. The studio process starts sessions as child processes (Claude Code) or runs its own agent loop against the Claude API, and starts renders (Remotion, ffmpeg, Chrome) one at a time.

flowchart LR
  subgraph machine ["Your computer"]
    subgraph node ["Node process: mortiflix serve"]
      srv["Web server :4646
JSON API + SSE"] rn["Runner"] rq["RenderQueue"] br["Bridge
(per session)"] end browser["Browser
web studio"] sock[("Unix socket
OS temp dir")] claudecli["claude -p
(optional bubblewrap sandbox)"] tools["Remotion · ffmpeg · Chrome"] comfy["ComfyUI + Qwen3-TTS
(optional, GPU)"] fs[("Studio folder
~/Mortiflix")] end anth["Anthropic API"] eleven["ElevenLabs API
(optional)"] browser -- "HTTP + SSE" --> srv rn -- "spawns" --> claudecli claudecli -- "mfx" --> sock --> br rn -- "agent loop (API backend)" --> anth claudecli --> anth rq -- "spawns, one at a time" --> tools claudecli -. "vo.mjs" .-> eleven & comfy node --- fs
UML · deploymentRemote access (--host 0.0.0.0) adds an access-token cookie; by default it binds to 127.0.0.1.
Storage

The studio folder

The split that matters: sessions write to projects/<id>/; only Mortiflix writes to state/<id>/. A submission's files are copied into the record when submitted, so what you reviewed can't change afterwards.

flowchart LR
  root["~/Mortiflix  ($MORTIFLIX_STUDIO)"]
  root --> cfg["config.json
settings, no secrets"] root --> sec["secrets.json (mode 600)
your keys, web token"] root --> env["session.env (mode 600)
other keys handed to sessions"] root --> chk["checks.json
the studio's error checklist"] root --> taste["TASTE.md
learned across projects"] root --> pl["pipelines/
your own (override built-ins)"] root --> projects["projects/‹id›/
working folder · session cwd"] root --> state["state/‹id›/
the record"] root --> runf["run/
runner lock, render logs, shared Remotion install"] projects --> w1["CLAUDE.md · JOURNAL.md · checklist.md"] projects --> w2["pipeline/ · .claude/skills/ · .mortiflix/GATES.md"] projects --> w3["input/ · feedback/ · out/ · video/"] state --> s1["project.json · events.jsonl"] state --> s2["pipeline/ (pinned snapshot)"] state --> s3["reviews/‹step›/v‹n›/ submission + copied files + feedback"] state --> s4["sessions/*.jsonl transcripts + activity"] classDef rec stroke:#b18cff,stroke-width:2px; class state,s1,s2,s3,s4 rec;
UML · packageHighlighted: the record, written only by the studio process.
Structure

Classes & data

The code is mostly functions over plain JSON records. The few real classes are the long-lived ones (Runner, RenderQueue, the API backend's Shell, the voice clients). Backends share one informal interface.

classDiagram
  direction LR
  class Runner {
    +root
    +renders: RenderQueue
    +current
    +loop(watch, pollMs)
    +runSession(projectId)
    +stopProject(id)
    +stop()
    emits change, activity
  }
  class RenderQueue {
    -jobs: Map
    -order: id[]
    +add(projectId, sessionId, label, argv, cwd, env) id
    +wait(id, projectId, seconds) view
    +get(id, projectId) view
    +stopSession(sessionId)
    -pump()
  }
  class Backend {
    <<interface>>
    +name
    +available(config, root) bool
    +run(root, projectId, workdir, prompt, env, transcript, onActivity, signal, config) Result
  }
  class ClaudeCode {
    +sandboxArgs()
    +describeTool()
  }
  class AnthropicApi {
    +DEFAULT_MODEL
    +credentials(root)
    +editorTool()
    +confine()
  }
  class Demo
  class Shell {
    +run(command, timeout)
    +restart()
  }
  class UserError
  class GateError
  class ElevenLabs {
    +account() models() voices()
    +library() dictionaries()
    +speak(text)
  }
  class ComfyUI {
    +nodes() status()
    +queue(graph)
  }
  Backend <|.. ClaudeCode
  Backend <|.. AnthropicApi
  Backend <|.. Demo
  AnthropicApi *-- Shell
  Runner *-- RenderQueue
  Runner ..> Backend : runs one session at a time
  UserError <|-- GateError
  Error <|-- UserError
        
UML · classBehavioural classes. A refusal is a GateError: a plain sentence the session can act on.
classDiagram
  direction LR
  class Project {
    id
    title
    pipeline (slug)
    backend?
    state
    answers
    steps: Map~key, StepRecord~
    questions: Question[]
    needs_you?
    no_progress
    without_gate
    usage
  }
  class StepRecord {
    state
    version
    last_feedback
  }
  class Pipeline {
    slug
    name
    makes
    shared_skills[]
    intake: IntakeField[]
    steps: PipelineStep[]
    status_lines
    checks: Check[]
  }
  class PipelineStep {
    key
    name
    review
    work[]
    after[]
    delivers
  }
  class Check {
    id
    title
    applies_to[]
    how
  }
  class Submission {
    note
    questions[]
    items[]
    error_checks[]
    pin_changes[]
  }
  class Feedback {
    verdict: approve|changes
    overall
    notes[]
    answers
  }
  class Note {
    text
    item?
    x?, y?
    time_sec?
    paragraph?
  }
  class Question {
    id
    text
    choices?
    default?
    answered_at?
  }
  Project "1" --> "1" Pipeline : pinned snapshot
  Project "1" *-- "*" StepRecord
  Project "1" *-- "*" Question
  Pipeline "1" *-- "1..*" PipelineStep
  Pipeline "1" *-- "*" Check
  StepRecord "1" --> "*" Submission : versions v1..vn
  Submission "1" --> "0..1" Feedback
  Feedback "1" *-- "*" Note
  Submission ..> Note : pin_changes answers each
        
UML · class (data)The JSON records in state/<id>/ and pipeline.json.
Behaviour

State machines

A project's state is recomputed from its steps after every change (settle()): queued when there's work the studio can do, waiting when it's your turn, delivered when every step is finished. Pause and cancel are yours; two sessions in a row without progress pause it automatically.

stateDiagram-v2
  [*] --> draft : createProject
  draft --> queued : startProject (required intake answered)
  queued --> waiting : settle - a step is in review or a question is open
  waiting --> queued : you approve / ask for changes / answer
  queued --> delivered : settle - every step finished
  queued --> paused : no progress twice, 8 sessions without a gate, needs-you
  queued --> paused : you pause
  waiting --> paused : you pause
  paused --> queued : you resume
  draft --> cancelled : you cancel
  queued --> cancelled : you cancel
  waiting --> cancelled : you cancel
  paused --> cancelled : you cancel
  delivered --> [*]
  cancelled --> [*]
        
UML · state machineProject (projects.STATES, gates.settle, runner guards).
stateDiagram-v2
  [*] --> blocked
  blocked --> ready : every step in "after" is finished
  ready --> working : mfx step start / first work
  working --> in_review : mfx submit (reviewed step)
  ready --> in_review : mfx submit
  in_review --> approved : you approve
  in_review --> changes : you ask for changes
  changes --> in_review : next version, every note answered
  working --> done : mfx step done --checks (internal step)
  approved --> [*]
  done --> [*]
  note right of blocked : derived states (blocked, ready) are computed by stepView; the rest are stored
        
UML · state machineStep. Steps with no dependency path between them run side by side (the explainer's script and style frames).
stateDiagram-v2
  [*] --> waiting : mfx render
  waiting --> running : pump() - nothing else running
  running --> done : exit 0
  running --> failed : exit ≠ 0
  waiting --> failed : session ended
  running --> failed : session ended (SIGTERM to the group)
  done --> [*]
  failed --> [*]
        
UML · state machineRender job. One render runs at a time across the whole studio.
Behaviour

One session, start to finish

The runner takes the oldest queued project, one session at a time (a run/runner.pid lock means serve and run never overlap). The session is told only to read CLAUDE.md, which torch.mjs writes fresh each time: where things stand, the steps table, what you said, the error checks, the brief (fenced as data), your taste and the journal.

sequenceDiagram
  autonumber
  participant R as Runner
  participant T as torch.mjs
  participant B as Bridge
  participant K as Backend
  participant S as Claude session
  participant M as mfx
  participant G as gates.mjs
  R->>R: next queued project
(oldest queued_at) R->>T: prepareWorkdir
(pinned pipeline, skills, GATES.md) R->>T: writeTorch → CLAUDE.md R->>B: openBridge (Unix socket + token) R->>K: run(prompt "Read ./CLAUDE.md…",
env MFX_SOCKET / MFX_TOKEN) K->>S: start
(claude -p or the API agent loop) loop until it reaches a gate S->>M: mfx status / checks / render … M->>B: POST {command, args, token} B->>G: same function, same rules G-->>S: result, or a refusal
as a plain sentence end S->>M: mfx submit step submission.json M->>B: submit B->>G: submit(): validate,
copy files into state/ S->>M: mfx handoff "…" S-->>K: ends its turn K-->>R: {ok, usage, cost_usd} R->>R: stop leftover renders,
close bridge R->>G: settle() →
waiting · queued · delivered R-->>R: guards: pause after 2 no-progress
sessions or 8 without a gate
UML · sequenceRunner.runSession(). A session past maxSessionMinutes is stopped; the next one resumes from the handoff.
Behaviour

A review round

You review in the web studio or with mortiflix review. Notes can point at an item, a spot on a picture, a moment in a video or a paragraph. The next version must answer each note in pin_changes, or it's refused.

sequenceDiagram
  autonumber
  actor O as Owner
  participant W as Web studio / CLI
  participant G as gates.mjs
  participant FS as Studio folder
  participant R as Runner
  participant S as Next session
  O->>W: pin notes (frame x,y · moment · paragraph)
verdict "changes" W->>G: respond(step, version,
{verdict, overall, notes, answers}) G->>G: step must be in_review at that version,
notes must point at real items G->>FS: state/…/feedback.json
projects/…/feedback/step-vN.json G->>G: step → changes, settle → queued G-->>W: SSE "change" R->>S: new session
(CLAUDE.md summarizes the notes) S->>S: sort each note:
error or taste, fix both opt an error no check would have caught S->>G: mfx propose-check
(approved once, every future video runs it) end S->>G: mfx submit v(N+1)
pin_changes: one entry per note G-->>O: back in review,
with what changed per note
UML · sequencegates.respond() is reachable only from the web API and the CLI, never from mfx.
The rules

The gates

Every rule is enforced by src/gates.mjs in the studio process. A refusal returns a sentence the session can act on, for example "pin_changes: note 2 from directions v1 has no answer".

flowchart TB
  start(["mfx submit step submission.json"]) --> internal{"step is internal?"}
  internal -- yes --> r1["refuse: use mfx step done --checks"]
  internal -- no --> note{"has a note?"}
  note -- no --> r2["refuse"]
  note -- yes --> qs{"questions valid?
text, unique ids, default ∈ choices"} qs -- no --> r3["refuse"] qs -- yes --> items{"every item: exists, a file,
inside the project (symlinks resolved)?"} items -- no --> r4["refuse"] items -- yes --> mode{"review mode satisfied?
frames→images · video→video · document→text"} mode -- no --> r5["refuse"] mode -- yes --> checks{"every check for the step's work
reported? fixed / n/a have a note?"} checks -- no --> r6["refuse"] checks -- yes --> lock["under the project lock"] lock --> st{"step state ok?
not blocked, not in review, not approved"} st -- no --> r7["refuse"] st -- yes --> pins{"last version had notes?
one pin_changes entry each, with a change and a status"} pins -- no --> r8["refuse"] pins -- yes --> copy["copy files into state/…/reviews/step/vN/
write submission.json"] copy --> done(["step → in_review · event SUBMITTED · settle"])
UML · activitygates.submit(), in the order the checks run.
RuleWhere
Only you approve a reviewed steprespond(): web API and CLI only
Internal steps finish only with their checksstepDone()
Every check for the step's kind of work is reportedvalidateChecks()
Every note on the last version gets an answersubmit() → pin_changes
Submitted files exist, are inside the project, and are copiedsubmit()
A step can't start before the steps it runs afterstepView(), submit(), stepStart()
Status lines come from the pipeline's whiteliststatus()
Behaviour

Renders

Heavy work (Remotion, anything that starts Chrome or the GPU) goes through mfx render. It queues the job studio-wide and returns an id at once; mfx render-wait waits up to a timeout. No tool call blocks longer than a backend allows, and two renders never fight for memory.

sequenceDiagram
  participant S as Session
  participant B as Bridge
  participant Q as RenderQueue
  participant P as Remotion / ffmpeg
  S->>B: mfx render --label "animatic"
-- npx remotion render … B->>Q: add(job) Q-->>S: {id: 3, state: waiting, renders_ahead: 1} Q->>Q: pump(): previous job finished Q->>P: spawn (own process group, log file) S->>B: mfx render-wait 3 B->>Q: wait(3, 100 s) P-->>Q: exit 0 Q-->>S: {state: done, output_tail} Note over S,Q: when the session ends, its waiting and running jobs are stopped
UML · sequencerenderq.mjs. The web studio shows "Waiting to render" or "Rendering: <label>".
Content

Pipelines

A pipeline describes how one kind of video gets made: pipeline.json (intake questions, steps, how each is reviewed, the error checks, status lines), PIPELINE.md (the craft every session reads first) and skills. A project pins a snapshot of its pipeline when it's created, so editing a pipeline only affects new projects.

flowchart LR
  brief["Brief
questions"] script["Script
document · script"] frames["Style frames
frames · stills"] animatic["Animatic
video · motion, audio"] build["Build
internal · motion, audio"] final["Final
video · delivers"] brief --> script & frames script & frames --> animatic --> build --> final classDef you stroke:#ff8fd1,stroke-width:2px; class brief,script,frames,animatic,final you;
UML · activity (step graph)The explainer. Pink: you review it. Script and style frames run side by side.
PipelineSteps you reviewError checksGood for
explainerbrief → script → style frames → animatic → final930 s – 2 min explainers, narrated or not
social-shortbrief → hook frames → final815–45 s vertical shorts, hook first, works with sound off
logo-stingdirections → final6a 3–8 s logo animation; the quickest real run
songblueprint → final7an instrumental song from a genre and a topic: researched genre, MIDI, an instrument per channel, a master

Shared skills (pipelines/_shared/skills/)

motion-designthe craft of motion: timing, easing, composition
remotion-motionthe Remotion template; setup.mjs installs it once per studio (was 748 MB per project)
voiceovervo.mjs narration (ElevenLabs or Qwen3-TTS), checked by speech-to-text; sound.mjs effects and music beds
musicthe music engine (strudel.mjs): genre → blueprint → score → MIDI → an instrument per channel → audition → master, or your own master from the MIDI pack; every stage checked and written down in MUSIC-SHEET.md
final-passqc.mjs: format, black or frozen frames, loudness, and a frame sheet Claude has to look at
Execution

Backends

A backend exports run(…) → { ok, error?, usage?, cost_usd? } and available(config, root). Adding one is adding a module to BACKENDS in runner.mjs.

BackendWhat runsWho pays
claude-codeclaude -p … --output-format stream-json in the project folder with the configured permission flags; the stream feeds the activity log; optional bubblewrap sandboxyour Claude plan
anthropic-apiMortiflix's own streaming agent loop: a persistent bash shell, a text editor confined to the project that returns images as image blocks, optional web search and fetch, adaptive thinking, prompt caching, server-side compaction, refusal fallbacks. Default model claude-opus-5-5your API key, per token
demono model: walks every gate with placeholder frames and a test-pattern video through the real bridgenobody
Audio

Narration

voice/index.mjs holds the studio's choice (none, elevenlabs or qwen) and hands a session MFX_VOICE, plus ELEVENLABS_API_KEY only while ElevenLabs is the engine. Sessions narrate with the voiceover skill's vo.mjs; every line is checked by speech-to-text and retaken if words go missing, and word timings drive the animation.

elevenlabsyour voices, the default voices or the Voice Library; Eleven v4 by default and every API option (models, stability and similarity, language, normalization, pronunciation dictionaries, formats, data-residency servers). Settings shows your plan and whether you may use the audio commercially
qwenQwen3-TTS through ComfyUI on your own GPU: free and private. Offered when it fits (4 GB for 0.6B, 8 GB for 1.7B with delivery instructions)
noneon-screen text and music
Interfaces

CLI, web studio & mfx

For you: mortiflix

init · doctor · demomake the studio and pick a backend · check the setup · a free walk-through with placeholder work
pipelines · new <slug>list and validate pipelines · start a project (asks the brief's questions)
run · serverun sessions until something waits on you · the web studio on 127.0.0.1:4646 (sessions start by themselves)
list · status · reviewprojects · one project · read the note, answer, pin notes, approve or ask for changes
pause · resume · cancelcontrol a project
keys · configyour keys (hidden input, checked with a free API call, saved mode 600) · settings
checks · voiceapprove proposed checks · narration setup

For you: the web studio

A plain node:http server and a no-build vanilla JS app (web/app.js). JSON API under /api, live updates over server-sent events (/api/events: change and activity), submitted media under /files/<id>/reviews/… with Range support.

EndpointDoes
GET /api/studio · PUT /api/configstudio info and backends · settings
GET /api/pipelines · GET /api/checks · POST /api/checks/:idpipelines · the error checklist · approve or reject a proposed check
GET|POST /api/projectslist · create
GET /api/projects/:id · POST …/files · POST …/startdetail · upload intake files · start
POST …/pause|resume|cancelcontrol
POST …/reviews/:step/:versionapprove or ask for changes (the only way past a gate)
POST …/questions/:qidanswer a session's question
/api/voice/…narration: settings, ElevenLabs account/models/voices/library/dictionaries/preview, Qwen status, a test line
GET /api/eventsserver-sent events

For the session: mfx

status <key> [--rendering]the one sentence you see while it works (from the pipeline's whitelist)
step start|done · checks <step>step progress · the error checks a step must report
submit <step> submission.jsonsubmit at a gate
ask · needs-you · propose-checka question with a default · a blocker only you can fix · a new error check
render -- … · render-wait <id>queue heavy work · wait for it
feedback · files · handoff · taste · logread your response · the submitted files · hand off · learn taste · log an event
Trust

Security

01
A session runs as you by default: treat it like a capable assistant logged in as you. With sandbox: true (Linux, claude-code) it runs in bubblewrap: system read-only, its own project read-write, Mortiflix code read-only, its socket; your home, other projects, state/ and secrets aren't there.
02
Even unsandboxed, the gates hold: no approving, no changing what was reviewed (it's copied to state/), no submitting files from outside the project.
03
Your words are direction; outside material is data. File contents, web pages and downloads are data, never instructions, and a session reports anything in them that tries to give orders.
04
Your keys, in your studio. Mortiflix has no accounts of its own: mortiflix keys (or Settings › Keys) asks for yours with hidden input, checks each with a free API call, and saves it mode 600. The API key is never returned by the API and sessions don't get it; the ElevenLabs key goes to a session only while it narrates with ElevenLabs. A project that needs a missing key asks before it starts.
05
The web server binds to 127.0.0.1; on another host every request needs an access token (HttpOnly, SameSite=Strict cookie); a wrong Host on loopback is refused (DNS rebinding); state-changing requests need an X-Mortiflix header.
Measured

Where the compute goes

Measured on a real 5-second logo sting (claude-code backend, Claude Opus 5.5): the model is the cost, everything else is noise.

Whole sting, brief → approved final2 sessions · 12.5 min · $3.52 at list price
Input tokens served from the prompt cache95%
Mortiflix's own text a session starts with~6.8k tokens
Studio process per web refresh~1 ms per project
Runner while idleone directory listing every 1.5 s
qc.mjs on a 60 s 1080p video2.2 s

Why sessions end at gates: waiting is free. A session that stayed alive to wait would pay for its whole context again on every check. Fewer, shorter sessions (specific briefs, defaults on every question, checks that catch mistakes before a revision round) lower every part of the bill.

Reference

Module reference

FileRoleLines
bin/mortiflix · bin/mfxentry points3 · 3
src/cli.mjsthe mortiflix command: init, new, run, review, serve, config, checks, voice…532
src/gates.mjsevery rule: step view, settle, submit, respond, checks, questions, pause/resume/cancel427
src/web/server.mjsHTTP + SSE web studio API, file serving with Range, guards364
src/backends/anthropic-api.mjsthe API agent loop, Shell, confined editor308
src/voice/elevenlabs.mjsElevenLabs client and request building217
src/voice/qwen.mjsGPU detection, ComfyUI client, speak/listen graphs186
src/runner.mjsRunner: one session at a time, guards, BACKENDS183
src/pipelines.mjslist, load, validate, hash and snapshot pipelines; checks per step181
src/torch.mjsprepare the working folder, write CLAUDE.md, append taste176
src/projects.mjscreate/start/list projects, the record, events, journal175
src/studio.mjsstudio paths, config, secrets, session env, JSON I/O, file locks, isInside149
src/backends/claude-code.mjsclaude -p runner, activity parsing, bubblewrap args134
src/mfx.mjsthe session's command-line client to the bridge117
src/renderq.mjsRenderQueue: one render at a time, studio-wide108
src/backends/demo.mjsscripted stand-in that walks every gate97
src/bridge.mjsper-session HTTP server on a Unix socket87
src/voice/index.mjsthe studio's narration choice and session environment83
web/app.js · app.css · index.htmlthe no-build web studio: projects, live log, review room835 · 234 · 26
harness/GATES.mdthe protocol every session follows—
test/flow, gates, CLI, web, API backend, skills, voice25 tests
Contributing

Extending it

A new pipeline (where help is most wanted)

cp -r pipelines/logo-sting ~/Mortiflix/pipelines/my-pipeline   # studio pipelines override built-ins
$EDITOR ~/Mortiflix/pipelines/my-pipeline/pipeline.json
mortiflix pipelines                                             # validates it
mortiflix new my-pipeline --backend demo                        # walk its gates for free

Wanted: music videos, product films, data stories, kinetic type, 3D in Blender, captions for existing footage. See docs/PIPELINES.md.

A new backend

A module exporting name, available(config, root) and run({ root, projectId, workdir, prompt, env, transcript, onActivity, signal, config }), added to BACKENDS in src/runner.mjs. The session reaches the studio only through mfx with the MFX_SOCKET/MFX_TOKEN it's given.

Further reading

Architecture · Gate protocol · Pipelines · Security · Compute · Voice · Contributing