Turn a page manifest (still image + audio per page) into one narrated presentation video (MP4). The server is a pure compositor: it neither renders slides nor synthesizes speech. The agent prepares the assets; the server muxes and concatenates them with ffmpeg.
main.go → cmd.Execute
cmd/ cobra: serve (default) / doctor / version
internal/
jsonrpc/ JSON-RPC 2.0 message types + standard codes
transport/ newline-delimited stdio framing (1 MiB/line)
mcpserver/ initialize / tools/list / tools/call routing;
RawResult sentinel for rich content; in-process Call
logging/ slog to stderr (+ optional rotated file)
toolerr/ structured {code,message,details} tool errors
config/ sectioned TOML, strict decode, ~ expansion
workspace/ os.Root-contained per-deck dirs; agent-prepared roots
manifest/ page manifest JSONL parser (Page)
master/ the render pipeline (ffmpeg/ffprobe)
tools/ MCP tools: get_usage, master (+ embedded usage.md)
Master.Build is the whole pipeline. Given a validated []manifest.Page:
- Preflight —
exec.LookPathffmpeg and ffprobe →ffmpeg_not_found. - Resolve + verify assets — for each page,
ResolveInside(lexical) thenVerifyRegular(Lstat viaos.Root). A symlink/non-regular entry →path_not_allowed(surfaced immediately); a missing file is collected and reported together asmanifest_incomplete. - Probe durations — ffprobe
format=durationper page audio; the sum is the reported total. - Render segments — one segment per page: still image looped, muxed with
the audio, cut at
-t <duration>, scaled to fit + padded onto the canvas, encoded to H.264/AAC/yuv420p with parameters from[video]. Every segment is encoded identically. - Concat — write the concat-demuxer list (quoting-safe), re-verify each
segment as a regular file pre-spawn, then join by stream copy (
-c copy).
The arg builders (segmentArgs, concatArgs, probeArgs, concatList) are
pure functions and are unit-tested directly. Runner abstracts command
execution so tests fake ffmpeg/ffprobe entirely.
Why two phases instead of one filtergraph: a per-page segment + concat-copy is simple, linear in pages, and robust; a single N-input filtergraph grows complex and re-encodes everything. Stream-copy concat requires codec-parameter uniformity, which is exactly what step 4 guarantees.
One workspace = one deck under <root>/<id>/. The root is either the server
default (~/.video-studio) or an agent-prepared absolute path passed as
workspace_root. Because agent-prepared roots are agent-writable, every
server-side file operation goes through os.Root, so a symlink planted inside
the workspace cannot make the server read or write outside it. ffmpeg cannot
inherit os.Root, so inputs are re-verified with Lstat immediately before the
spawn; the residual verify-to-spawn race is accepted under the local
single-user threat model.
Tool errors are toolerr.Error{code, message, details}. errors.Is matches by
code, so sentinels compose regardless of message. Codes: invalid_arguments,
missing_argument, invalid_workspace_id, path_not_allowed,
invalid_manifest, manifest_incomplete, probe_failed, ffmpeg_failed,
ffmpeg_not_found, workspace_failed. The recovery table in
internal/tools/usage.md is coherence-tested against these.
- Hermetic —
go test ./...needs no ffmpeg.mastertests use a fakedRunnerthat returns a canned duration for ffprobe and materializes the output file for ffmpeg. - Pure-function — arg builders and the manifest parser are tested directly.
- In-process tool tests —
mcpserver.Server.Callinvokes a tool by name, sotoolstests exercise the real handler path (including workspace_root and symlink rejection) without stdio framing. - Real ffmpeg — validated manually on darwin (differently-sized slides letter/pillar-boxed onto 1920×1080; 1s + 2s audio → ~3s MP4).