Skip to main content
Every stage as a command, the files each one reads and writes, and how to work an episode from a terminal. The editor (EDITOR.md) does the same through its own server; both write the same files. seriesmith with no arguments lists every command, and each has --help.

Every command

build, check, refs, assemble and all take several episode folders at once, or a parent with --each; render and master take several folders.

The modules

seriesmith/core/ (parser, story IR, series config, speech pacing), jobs.py, takes.py, refs.py, source.py (the authored files), track.py (recordings and alignment) and peaks.py (waveforms) are the library the commands and the editor’s routes share. backends/ (local, cloud, deployments, Router), queue.py and runner.py (the job queue, and what puts renders, refs, upscales, posts and tours on it), finishing.py (the upscale and post graphs made portable) and server/ (the editor’s server) are how work gets done; comfy_nodes/ is what runs inside ComfyUI. settings.py is this machine’s settings, limits.py a model’s limits, and router_inspect.py reads a Comfy Router model’s schema (the editor’s Models window; no command of its own).

What you write

  • series.json — the series config: style, characters and props (with a design sentence each), locations (one entry per camera angle, with the light), voices, audio mode, and optionally the series target, render profiles, the refs image targets and a negative prompt. A character who changes clothes gets a second entry with of: — a wardrobe variant, its own design and its own sheet, everything else inherited. See examples/series_example.json.
  • epNN.md — the script: # sq sequences, ## sh shots with who:, with:, size:, dur: or audio:, plate:, camera:, sound:, music:, optionally target:, first: / last:, action prose and NAME: lines. See examples/script_example.md.
  • negative.txt (optional, beside the script): the episode’s negative prompt, for the targets that take one.
AUTHORING.md is the full format: every field, choosing a target, keyframes, the frame grids, the syllable budget, the camera vocabulary, how to break a scene into shots, giving two people in one scene an angle each, and the reference-slot rules. Read it once before writing an episode. Only three things are actually required of a script: the = ep01 Title header, at least one # sq01 <location> naming a location the series config defines, and per ## shNNN a unique id, action or dialogue, and a length (dur: seconds / auto / model, or an audio: window). Everything else has a default — a speaker joins the cast from their own NAME: line, size: is medium, plate: is the sequence’s location, and the camera holds still. The series config must have a series block, style.look, subjects (each with name, and design unless they are only ever a voice), and locations with a description and a plate each. --check names whichever is missing. Drafting with an AI assistant: python tools/make_prompts.py packages the guide as a skill, seriesmith-episode-script. build/skill/seriesmith-episode-script/ (generated, not in the repo) is the whole skill (the guide, a worked screenplay conversion from docs/SCRIPT_CONVERSION.md, and a copy of build to validate with): copy the folder into .claude/skills/ for Claude Code, or upload build/skill/seriesmith-episode-script.zip on claude.ai. prompts/SKILL.md is the skill’s text alone, and prompts/seriesmith-script.instructions.md the same guide for Cursor, Copilot or any system prompt. Edit the docs, not the copies. Whatever writes the script, --check and --pace are what say it is correct. Change the series config or script and run build again rather than editing the generated files: steps:, model:, lora:, profile: and target: are script fields, so experiments survive a rebuild, and overrides.json holds the editor’s tweaks. If you do hand-edit a shotlist, all --skip-build and the individual stages leave it alone.

build

  • Snaps every shot to its target’s legal lengths (H3: 17k+5 frames at 24fps, so 2.33, 3.04, 3.75, 4.46, 5.17s…; LTX: 8k+1; Wan: 4k+1).
  • dur: auto sizes a shot from its dialogue. --pace flags lines crammed into too short a shot.
  • Seeds come from the episode, sequence and shot IDs, so untouched shots render identically after edits.
  • refs_todo.md lists every image and voice sample still missing, with the prompt to make it.
  • Writes defaults.steps, defaults.model and defaults.lora into each shotlist, and warns when a shot overrides them or when the step count does not match the LoRA.

Steps, model and LoRA

Each pass has its own sampling setup, and the render stage applies it per shot, in memory — the workflow file on disk is never rewritten. The table is H3 Ref2VA’s; every target has its own presets in its target.json, and a series written for H3 doesn’t hand another target H3’s model, LoRA or steps. A distilled LoRA is trained on a fixed set of timesteps, so it does its best work at its own step count: the 8-step v1.0 was distilled at 768p for 8 steps, and the proxy keeps the 4-step v0.1 both for speed and because its 544p training is closer to the animatic’s 512×288. Override per pass in the series config (series for the final, proxy for the animatic):
Override one shot — or one sequence, by putting the line under the # header — in the script:
lora: none (or off) sets the LoRA strength to 0 instead of swapping files, which renders that shot on the base model — the honest comparison for what the turbo LoRA costs you. A shot’s line beats its sequence’s, which beats the pass default. --check lists every override so an experiment left in a script cannot surprise you later. Names must match ComfyUI’s dropdowns exactly, subfolder prefix included. A wrong name fails the first shot instead of quietly rendering with something else. ComfyUI reloads whenever the model or LoRA changes, so group experiments rather than scattering them through an episode.

kreagen

  • Each take is a job on this command’s own queue: --backend picks where (local, cloud, deployment:<target>; default each image target’s own, so a hosted one goes to Router). --target picks the image model, a hosted one included.
  • Skips anything already on disk. --redo makes a new take (kept in refs/_takes/) and leaves the live file alone unless you add --pick; combine it with --only. --all works from the whole series config instead of refs_todo.json.
  • --only matches part of the file path, so dean also matches dean_grown — which is also how a character and their wardrobe variants regenerate together.
  • Wardrobe variants come second. A variant’s views are edited from the character’s own (see Reference sheets), and a whole pass plans every job before any take is picked — so in the first pass the variant has nothing to edit from and is drawn cold. Run the pass, let the character’s views pick (anything with no live file takes its first usable candidate automatically), then regenerate just the variant on a model that reads references: --only gina_towel --target flux2_klein_edit --redo. Set that target once on the ref instead — in the editor’s per-ref target picker — and every later generate of it uses the edit model while everything else stays on krea2. The new take does not go live on its own once the variant already has a file: pick it (--pick, or in the Refs tab).
  • Voices. Images only, unless --voices: then each character’s voice ref is generated on the audio target (--voice-target, --voice-seconds; default ltx2_voice) into refs/voices/<char>.wav, as takes like any other ref. --from-take sh020:2:1.5-6.0 needs no model at all — it cuts those seconds out of a rendered take’s sound into a voice candidate.
  • --discard subject:ada:02_side:2 moves a candidate to refs/_takes/_trash/ (nothing is deleted; if it was the pick, the ref is cleared).
  • --edit location:kitchen --instruction "night, the lights off" changes a picture the ref already has on an edit model, into new candidates (--with brings in another ref’s picture, --count, --seed, --as-typed).
  • Each character is made as four square views sharing a seed, then joined by mksheet. Every view is a take in refs/_takes/subject__<char>/, so one angle can be redone and re-picked in the editor’s Refs tab (seriesmith/refs.py).
  • Negative prompts do nothing on krea2 turbo: no negative field, and no guidance branch at cfg 1. kreagen leaves a workflow’s negative side as saved, so a NAG or negpip setup passes through; --negative-file only bites above --cfg 1.0, on a model that expects guidance. H3 takes no negative prompt at all. Details in KREAGEN.md. The other image targets take the episode’s negative.txt or the series config’s negative (inert at cfg 1 too); --negative-file beats both for its run.
  • --target z_image_turbo (or any image target) draws with another model; --clear location:kitchen unpicks a ref (its file goes, the takes stay).
  • No style LoRA by default. --lora <file> --lora-strength 0.7 adds one, --unet swaps the image model. Match the LoRA to the look in series.json: a realism LoRA helps live action and hurts a flat 2D show.
  • With krea2, drives your own ComfyUI workflow when it finds one. Save a text-to-image graph as krea2_refs_t2i.json among ComfyUI’s workflows (or set $KREA_WORKFLOW, or pass --workflow), and kreagen sets the prompt, size, seed, steps and cfg on it per image instead of using its built-in graph. That is the no-Python way to change the model, LoRA, sampler or scheduler for reference art. --no-workflow forces the built-in graph. The graph needs one KSampler, one EmptyLatentImage and one SaveImage; positive and negative are found by following the sampler’s own links. At cfg 1.0 the negative encoder is replaced with ConditioningZeroOut, since guidance is off and encoding it would be wasted; above 1.0 with --negative-file, a graph wired straight to ConditioningZeroOut gets a text encoder put back. seriesmith/targets/image/krea2/workflow.json is a working example with no LoRA — point its loaders at your own models. --lora splices a LoRA node into a graph that has none, or sets the one that is already there. If a graph carries a step-distilled LoRA, pass the matching --steps.

render

  • Runs each shot through its target on its backend (--backend, else the shot’s, the episode’s, the series config’s series.backend, else the target’s own; see SETUP.md; seriesmith backends lists them). A workflow target’s graph goes as a job, with the shot and its reference pictures as uploads; the files that come back are written into the take. A hosted target (Seedance, Kling, Veo, hosted H3, …) is one Router call, and its takes are marked reproducible: false. Each take is a job on this command’s own queue, run one at a time and waited for; a continuous: latent shot holds the take rendered just before it.
  • It runs anywhere the episode folder is: the backend never reads it. It checks the render folders to skip finished shots, so Ctrl-C (which cancels the running job) and re-running resumes where it stopped.
  • Every take gets a sidecar, <shot>_tNN.json, holding its seed, model, LoRAs, steps, the refs it used (with checksums) and its status (queued, ok, failed). It also gets <shot>_tNN.shotlist.json, the exact shot it rendered. Nothing about a take is lost when the script changes.
  • --redo renders a new take (t02, t03…) and never overwrites. --take N overwrites that take.
  • Seeds: a shot’s first take uses its built seed. --redo picks a new seed, so a redo is really a new attempt. --same-seed keeps the built one, --seed N uses N, and --new-seed forces a fresh one.
  • Takes the model, LoRA and step count from each shot, falling back to the shotlist defaults, then applies the episode’s overrides.json (see seriesmith override below). --model, --lora and --steps override all of that for one run. --lora repeats to stack LoRAs (--lora a.safetensors --lora b.safetensors:0.6); the extras are chained after the workflow’s LoRA loader. --note stores a remark in each take’s sidecar. It prints what it will use, and warns when one episode needs more than one model or LoRA.
  • Targets. Each shot renders with its own target: H3 shots through the shot loader (the shot as JSON, its pictures as uploads), ltx2 shots with every value patched into the LTX-2.5 graph, a hosted target as a model call. --target ltx2 renders the chosen shots on another target for this run (their story is compiled for it on the spot, no rebuild); seriesmith override <ep> <shot> --target ltx2 does it for every run. An LTX shot’s keyframes (refs/shots/<shot>/first.png, last.png) go up with its job. --dry-run writes what the first shot would send (render_graph.json, or render_call.json for a hosted model) and runs nothing.
  • Failed shots are reported and skipped; --stop-on-error halts instead. A shot whose reference images are still missing is blocked, and the report names the exact files; --allow-missing-refs renders it against flat grey stand-ins.
  • Other flags: --panel-mode, --save-frames, --latent / --no-latent, --size WxH, --allow-model-mismatch.
  • Each target’s workflow comes from the running ComfyUI’s saved workflows (the name in its target.json binding: H3_Ref2VA_Shotlist_v1.json for H3, krea2_refs_t2i.json for reference images), so it always matches your ComfyUI’s node versions; failing that, $COMFYUI_PATH’s workflows folder, then the copy in this repo. The target’s own env var ($SERIESMITH_MINIMAX_H3_REF2VA_WORKFLOW, $KREA_WORKFLOW, …) beats all of those (kreagen also takes --workflow). Every target’s name and variable is in INSTALL.md; seriesmith targets --install-workflow <target> copies a target’s graph into ComfyUI to edit on the canvas. Keep the saved krea2_refs_t2i.json free of style LoRAs (experiment under another name), since the references must follow the series config’s look.
  • It converts the canvas workflow to API format itself. If ComfyUI rejects it, save Workflow → Export (API) and point the target’s env var at that file.

assemble

  • Follows the episode’s cut.json if there is one (a list per pass of {"shot", "take", "pass", "trim_in", "trim_out", "audio"} entries). The list sets the order and, with take: N, the exact take. Without cut.json, or for a shot the list leaves out, a shot goes in script order and uses its latest usable take: status ok and the mp4 is there, so a --redo still queued or failed is skipped. Takes from before sidecars count as ok.
  • A picked take that is queued, failed or has no mp4 makes the shot missing, and the report says why. --take N forces take N for every shot and beats cut.json.
  • Entries for shots deleted from the script are skipped with a note. An entry with "pass": "proxy" in the final list is a placeholder: the proxy take is scaled to the final size. trim_in/trim_out drop frames from the head and tail.
  • The pass comes from the shotlist’s name (_proxy means proxy); --pass overrides it.
  • --check lists the resolved cut (order, take, pass, trims, placeholders, orphans) and writes nothing. --name sets the output filename, --shotlist picks the proxy files, --subfolder reads this pass’s takes from another folder.
  • Mute clips (dub, clone) get their _h3.wav added so the join works. --audio auto|mp4|h3|none|master chooses the sound for the clips that have none of their own. A clip can have its own: cut.json’s audio lays another take’s sound, a file’s, or silence under it, cut or padded to the clip so the length never changes (the Timeline’s Audio from…, or seriesmith cut --audio). --audio master and none override those; the others leave them alone.
  • Shots with an audio: window are trimmed to it (re-encoded, x264 CRF 16) so the cut lines up with the recording. --no-trim keeps the padding.
  • epNN_shots.txt lists every shot’s start time in the cut.
  • --upscaled (either pass) plays each clip’s fresh upscale (seriesmith upscale) and writes epNN_up.mp4 at the upscales’ size beside the plain cut; a clip without one is scaled up and named in the report. --size WxH sets the cut’s size (1920x1080 for a 1920×1088 upscale), letterboxing a clip whose aspect differs.
  • This is a review cut. Mix the final audio in Resolve.

Takes, picks and overrides

  • takes marks a take stale when rendering the shot now would differ: script (the shot changed in the script or series config), ref (a reference image or voice changed) or preset (model, LoRA, steps or size defaults changed). Stale is a hint, not an error.
  • overrides.json is how you tweak one shot without touching the script: the exact prompt, a seed, model, LoRAs, steps. Prompt, model, LoRAs and steps are per pass (final unless --proxy; --both sets both), because the proxy’s prompt differs when it generates its audio. The seed applies to both passes. A rebuild never loses an override. If the shot has changed since you wrote it, takes and override flag it STALE, and it is still applied.
  • Model, LoRAs and steps belong in the script when they’re decisions about the film. overrides.json is for the tweak loop. When a tweak turns out to be a decision, seriesmith promote moves it into the script or the series config for you — it prints the plan and the diffs first, and --all (or --item shot:sh020:steps) writes them, drops those overrides and rebuilds. The old files are kept in <episode>\_history\.

Editing the cut

cut.json holds it all — order, the take each shot uses, trims, locks and per-clip audio — per pass, and assemble follows it. The editor’s Timeline writes the same file.

Finishing: upscale, post, master, publish

  • upscale refines each take of the pass’s cut that has no fresh upscale into <stem>.up.mp4 beside it: --method latent (re-sampled late in the take’s own schedule; the default where the target has one), pixel (an upscale model; --pixel-model rtx_vsr for NVIDIA RTX Video Super Resolution) or seedvr2. --start-step, --scale, --vae, --deliver WxH, --quality review|master, --redo.
  • post finishes an upscale into <stem>.post.mp4: --enhance draft|production|cinematic (or pixel|seedvr2|supir|none), --blur, --grain; with neither, the series config’s post.master recipe (POST_PROCESSING.md).
  • master runs the series config’s upscale.master recipe (and post.master with --post) on every shot of the cut, waits for them, then assembles and titles the master into <episode>/master/. --conform, --allow-gaps, --order cut|target, --prores / --prores-only.
  • publish puts the show’s _titles/INTRO.mp4 and OUTRO.mp4 around a cut (--input, default the review cut) with the episode title, into <episode>/publish/.
  • upscale, post and master queue on a backend like a render (--backend, default local); publish is ffmpeg on this machine.

Models

  • target-from-workflow reads a saved workflow (a name among ComfyUI’s, or a .json path) against the running ComfyUI’s /object_info, and proposes a video or image model: which widget takes the prompt, size, length, seed, steps and each model file, and which output node saves the take. --save writes it into <show>/targets/<id>/target.json.
  • A model for every show, a Comfy Router model, limits, and switching on a built-in draft are done in the editor’s Models window (EDITOR.md, “The Models window”).

Backends and the editor’s server

  • backends reads this machine’s settings and COMFY_API_KEY (SETUP.md): local always, cloud and router with a key, a deployment:<target> per deployment.
  • serve starts comfy-api-proxy for the local backend and picks up the jobs the last run left. --port (default 8190), --host, --root (repeatable), --auto-launch (open the editor in the browser once it is listening, as the .bat launchers do).
  • stop asks the server on this machine to stop (--port); --restart restarts it when it runs under run_seriesmith.bat / run_seriesmith_ext.bat (a bare seriesmith serve just stops). Jobs still running on a backend are picked up on the next start.

Recorded dialogue (lip sync)

Full guide: RECORDED_DIALOGUE.md.
  • The editor does the same from its Recording window: attach the file (browse the ComfyUI machine, type a path, or drop it in), then align, with progress and the report. It runs align in whichever Python has faster-whisper and numpy — see INSTALL.md if it picks the wrong one.
  • align transcribes the recording with word timings, matches it to the script, and cuts it into one continuous run of windows with cuts at the quietest point of each pause.
  • It writes audio: in-out on every shot, points the series config at the recording, keeps the old copy of each in <episode>\_history\ (the editor’s history), and writes align_report.md.
  • Speaking shots then default to dub_keep_foley: H3 lip-syncs to your line and keeps its own sound effects as _foley.wav.
  • The retention setting controls how much of your line H3 copies:
Set it per shot (retention: fully_copy) or for the episode ("audio": {"retention": ...} in the series config). clone and generate shots don’t use it.

Project layout