make-ai-short — generative shorts, end to end
The pixels come from a video model; Remotion only composites clips + captions. Proven on
blue-man #1 "The Door". Sibling of /make-short (100% TSX) — same discipline, plus three
gates TSX never needed: cost stated before generating, character locked from
character.json, loop pinned via end-frame conditioning.
Run everything from the repo root. Needs FAL_KEY (clips + images) and ELEVENLABS_API_KEY
(voice) in .env.
The three iron rules
- NEVER generate a character from text twice.
ai-shorts/<char>/character.json +
character.png is the locked reference. Every still that needs the character is
gen_image.py --ref character.png; every clip is image-to-video from a frame that already
contains him. If a character sheet doesn't exist yet, making ONE (and getting the user's
eye on it) is its own step before anything else.
- STATE THE COST BEFORE SPENDING IT. Derived, not quoted: Seedance 1080p audio-off =
h×w×24×dur/1024 tokens × $1.2/1M ≈ $0.0583/s (fal pricing API validated 2026-07-14 —
see ai-shorts/IDEAS.md for the full table + the Wan/Veo-Lite traps). Always
generate_audio:false — voice is ElevenLabs, sound is our SFX pipeline.
- Loop by CONSTRAINT, not luck. Shot 1's true frame 0 (extracted from the clip, not the
prompt image) is the loop target. The final shot passes it as
end_image_url
(Seedance/Kling), and the composition settles onto that exact still over the last ~10
frames (LoopSettle pattern in remotion/src/shots/ai-1/Ai1Door.tsx).
Artifact contract
ai-shorts/<name>/
script.md · beats.json — same contract as /make-short (vo[] with real word times)
character.json — THE LOCKED REFERENCE (+ video_model block = the picked model)
shots/NN-slug.png — per-shot START frame (gen_image.py --ref character.png [--ref scene])
shots/NN-slug.json — gen_clip sidecar (model, payload, request id)
shots/upload-urls.json — fal storage urls (uploads are reusable across re-rolls)
voice/ · sfx-plan.json · output/ [voice/output gitignored]
remotion/src/shots/ai-N/ — composition + vo.gen.ts
media/projects/<name>/ — THE clip copies for staticFile() (committed — pixels are
not reproducible; shots/*.mp4 working copies are gitignored)
Order of operations (voice before pixels)
- Script — script.md + beats.json. Hook composed at frame 0; thesis should WANT the loop
(blue-man #1: "arrival is a myth" → ending = walking toward the same door). ~80-100 words,
2.5-2.7 wps windows. No engagement-CTA outro, ever — end on the payoff, loop is the ending.
- Voice FIRST —
python tools/gen_voice.py --beats ... --emit-ts remotion/src/shots/ai-N/vo.gen.ts
(George JBFqnCBsd6RMkjVDRZzb + eleven_v3 tags for narrators). Real word times decide the
shot windows; Seedance takes integer 4-12s durations, so round shot lengths UP from windows.
- Start frames — one per shot,
python tools/gen_image.py --model pro --aspect 9:16 --ref character.png
(+ the shot-1 scene still as a 2nd ref when a location/prop must match across shots).
QA every PNG before any video call — this is where drift is cheap to fix.
- Watch hands, clothing details, proportions — name them explicitly in the prompt on re-rolls.
- Geometry must be walkable (the shot-6 lesson, cost 2 re-rolls): with an end-frame pin,
the start frame must let the motion REACH the end state by walking forward — if the end
frame's character is CLOSER to camera than the start's, the model spawns a clone instead.
Start him larger/nearer than the end state so forward motion shrinks him into it.
- Clips — state the total derived cost, then
python tools/gen_clip.py per shot in parallel
(--set image_url=<fal storage url> --set 'duration="N"' --set resolution=1080p --set generate_audio=false, final shot --set end_image_url=<frame0 url>). Upload refs
once via bakeoff_clip.upload_ref; keep urls in shots/upload-urls.json.
Prompt every clip with: exact character phrase + "EXACTLY ONE character in the scene for
the entire shot" (clones are the #1 failure) + camera move + "no morphing" + style line.
- Clip QA, frame by frame — start/mid/end strip per clip + a side-by-side of the final
clip's last frame vs the loop target. Re-roll failures (state the re-roll cost); a 5s
1080p re-roll is ~$0.29 — cheap next to shipping a clone.
- Composition — Sequences of
OffthreadVideo over ONE timeline, ~8-frame crossfade
underlaps (clips are ~1s longer than their windows — that's the slack), Captions +
ProgressBar at root, LoopSettle dissolve at the tail. Copy final clips to
media/projects/<name>/. npm run gen, frames.mjs at boundaries + heroes, READ every PNG,
then render-all --scale=1 and mux voice.wav.
- SFX — /suggest-sfx flow. Gappy-VO calibration (blue-man #1): cues in VO gaps get no
masking and no duck — they need ~7-8 dB LESS than the dense-narration table (gap
whooshes -9..-13, not -3..-5). Verify with volumedetect RMS against a speech-mean
reference; hero layer may kiss voice level once. Audition mix awaits the user's ear
(hard gate).
- Save back — catalog used_in, beats.json real timings (gen_voice writes them), sidecars
committed, cost ledger in the final report.
Done =
character consistent in every frame of every clip (frame-by-frame QA'd) · loop verified
(last frame vs frame 0 side-by-side) · captions on real word times · costs stated before each
spend and totalled after · SFX audition rendered, awaiting the user's ear · no CTA outro.
1---2name: make-ai-short3description: Build a GENERATIVE-pixels vertical short (1080×1920, ~35-40s) end-to-end — a recurring locked character animated by a fal video model (Seedance 1.5 Pro default) under a philosophical/story VO, composited in Remotion with word-synced captions, SFX audition, seamless frame-0==last-frame loop. Use when the user wants to "make an AI video short", "make an ai short", "make a blue-man video", or any short where the pixels come from a VIDEO MODEL — not TSX animation (that is make-short) and not layered collage (that is make-vox). Defers model choice re-litigating to ai-shorts/IDEAS.md, TSX crash rules to vidtsx-2d-generator, SFX taste to suggest-sfx + brand §7.4---56# make-ai-short — generative shorts, end to end78The pixels come from a video model; Remotion only composites clips + captions. Proven on9blue-man #1 "The Door". Sibling of `/make-short` (100% TSX) — same discipline, plus three10gates TSX never needed: **cost stated before generating**, **character locked from11character.json**, **loop pinned via end-frame conditioning**.1213Run everything from the repo root. Needs `FAL_KEY` (clips + images) and `ELEVENLABS_API_KEY`14(voice) in `.env`.1516## The three iron rules17181. **NEVER generate a character from text twice.** `ai-shorts/<char>/character.json` +19 `character.png` is the locked reference. Every still that needs the character is20 `gen_image.py --ref character.png`; every clip is image-to-video from a frame that already21 contains him. If a character sheet doesn't exist yet, making ONE (and getting the user's22 eye on it) is its own step before anything else.232. **STATE THE COST BEFORE SPENDING IT.** Derived, not quoted: Seedance 1080p audio-off =24 `h×w×24×dur/1024` tokens × $1.2/1M ≈ **$0.0583/s** (fal pricing API validated 2026-07-14 —25 see ai-shorts/IDEAS.md for the full table + the Wan/Veo-Lite traps). Always26 `generate_audio:false` — voice is ElevenLabs, sound is our SFX pipeline.273. **Loop by CONSTRAINT, not luck.** Shot 1's true frame 0 (extracted from the clip, not the28 prompt image) is the loop target. The final shot passes it as `end_image_url`29 (Seedance/Kling), and the composition settles onto that exact still over the last ~1030 frames (`LoopSettle` pattern in `remotion/src/shots/ai-1/Ai1Door.tsx`).3132## Artifact contract3334```35ai-shorts/<name>/36 script.md · beats.json — same contract as /make-short (vo[] with real word times)37 character.json — THE LOCKED REFERENCE (+ video_model block = the picked model)38 shots/NN-slug.png — per-shot START frame (gen_image.py --ref character.png [--ref scene])39 shots/NN-slug.json — gen_clip sidecar (model, payload, request id)40 shots/upload-urls.json — fal storage urls (uploads are reusable across re-rolls)41 voice/ · sfx-plan.json · output/ [voice/output gitignored]42remotion/src/shots/ai-N/ — composition + vo.gen.ts43media/projects/<name>/ — THE clip copies for staticFile() (committed — pixels are44 not reproducible; shots/*.mp4 working copies are gitignored)45```4647## Order of operations (voice before pixels)48491. **Script** — script.md + beats.json. Hook composed at frame 0; thesis should WANT the loop50 (blue-man #1: "arrival is a myth" → ending = walking toward the same door). ~80-100 words,51 2.5-2.7 wps windows. **No engagement-CTA outro, ever** — end on the payoff, loop is the ending.522. **Voice FIRST** — `python tools/gen_voice.py --beats ... --emit-ts remotion/src/shots/ai-N/vo.gen.ts`53 (George JBFqnCBsd6RMkjVDRZzb + eleven_v3 tags for narrators). Real word times decide the54 shot windows; Seedance takes integer 4-12s durations, so round shot lengths UP from windows.553. **Start frames** — one per shot, `python tools/gen_image.py --model pro --aspect 9:16 --ref character.png`56 (+ the shot-1 scene still as a 2nd ref when a location/prop must match across shots).57 **QA every PNG before any video call** — this is where drift is cheap to fix.58 - Watch hands, clothing details, proportions — name them explicitly in the prompt on re-rolls.59 - **Geometry must be walkable** (the shot-6 lesson, cost 2 re-rolls): with an end-frame pin,60 the start frame must let the motion REACH the end state by walking forward — if the end61 frame's character is CLOSER to camera than the start's, the model spawns a clone instead.62 Start him larger/nearer than the end state so forward motion shrinks him into it.634. **Clips** — state the total derived cost, then `python tools/gen_clip.py` per shot in parallel64 (`--set image_url=<fal storage url> --set 'duration="N"' --set resolution=1080p65 --set generate_audio=false`, final shot `--set end_image_url=<frame0 url>`). Upload refs66 once via `bakeoff_clip.upload_ref`; keep urls in shots/upload-urls.json.67 Prompt every clip with: exact character phrase + "EXACTLY ONE character in the scene for68 the entire shot" (clones are the #1 failure) + camera move + "no morphing" + style line.695. **Clip QA, frame by frame** — start/mid/end strip per clip + a side-by-side of the final70 clip's last frame vs the loop target. Re-roll failures (state the re-roll cost); a 5s71 1080p re-roll is ~$0.29 — cheap next to shipping a clone.726. **Composition** — Sequences of `OffthreadVideo` over ONE timeline, ~8-frame crossfade73 underlaps (clips are ~1s longer than their windows — that's the slack), `Captions` +74 `ProgressBar` at root, `LoopSettle` dissolve at the tail. Copy final clips to75 `media/projects/<name>/`. `npm run gen`, frames.mjs at boundaries + heroes, READ every PNG,76 then render-all --scale=1 and mux voice.wav.777. **SFX** — /suggest-sfx flow. **Gappy-VO calibration (blue-man #1):** cues in VO gaps get no78 masking and no duck — they need **~7-8 dB LESS** than the dense-narration table (gap79 whooshes -9..-13, not -3..-5). Verify with volumedetect RMS against a speech-mean80 reference; hero layer may kiss voice level once. Audition mix awaits **the user's ear**81 (hard gate).828. **Save back** — catalog used_in, beats.json real timings (gen_voice writes them), sidecars83 committed, cost ledger in the final report.8485## Done =8687character consistent in every frame of every clip (frame-by-frame QA'd) · loop verified88(last frame vs frame 0 side-by-side) · captions on real word times · costs stated before each89spend and totalled after · SFX audition rendered, awaiting the user's ear · no CTA outro.