AI Film Studio
End-to-end AI film production. The agent handles the creative decisions (reference analysis, story, prompts, pacing);
pre-built scripts handle the API plumbing. Works for short films, ads, product promos, brand films and social reels —
the pipeline is the same, only Step 0 (inspiration) and the assembly template (end card, CTA, aspect) change.
Intake — route before you generate anything
Classify the deliverable first. Only the last two rows need generative video models; everything else is HyperFrames alone.
| The user wants… |
Route |
Why |
| Motion graphics only — kinetic type, logo sting, stat count-up, chart, lower-third, animated headline/tweet, transparent overlay (usually < 15 s, no footage) |
/motion-graphics (official HyperFrames workflow skill; if missing: npx hyperframes@0.8.27 skills update motion-graphics) |
zero generation cost; deterministic, editable HTML |
| A promo/site tour from a website URL |
/product-launch-video |
captures the real site; no AI footage needed |
| A narrated explainer from text/notes, no URL |
/faceless-explainer |
HyperFrames generates its own visuals |
| Existing footage that needs captions or graphic packaging |
/embedded-captions (subtitles) · /talking-head-recut (titles, lower-thirds, callouts) |
footage untouched |
| A deck / presentation |
/slideshow |
navigable deck, not a render |
| Beat-synced cut of images/clips to a music track |
/music-to-video |
beat grid drives the edit |
| Film / ad / promo / reel with generated footage |
this skill, Steps 0–7 |
needs stills + video models |
| Hybrid: generated plates plus heavy graphics (kinetic type over AI footage, data callouts, shader transitions) |
Steps 0–5 here for the clips, then /hyperframes-animation blueprints + /hyperframes-core instead of build_hyperframes_timeline.py |
the simple timeline builder only does cards, overlays, captions and crossfades |
Rules of thumb:
- If no shot needs photographed-looking content, do not open a video model. Type, shapes, charts and logos are HyperFrames' job.
- Diegetic vs. graphic. Omni/Seedance may render effects that live inside the footage (smoke forming a shape, light writing a word, particles becoming a product). Anything that must be exact and editable — brand typography, prices, claims, CTAs, captions, lower-thirds — is composited in HyperFrames, never generated. Video-model text drifts frame to frame and cannot be fixed afterwards.
- Workflow skills (
motion-graphics, product-launch-video, faceless-explainer, embedded-captions, talking-head-recut, slideshow, music-to-video, general-video, media-use) install on demand: npx hyperframes@0.8.27 skills update <name …> (the core hyperframes-* set alone does not include them). Check with ls ~/.claude/skills.
- When routing to an official HyperFrames skill, hand over the brief (product, duration, aspect, brand colours/fonts, any
STYLE_LOCK from Step 0) and let that skill run its own loop (init → lint → check → preview → render). Come back here only if it turns out real footage is needed.
- Unsure? Ask one question: "Does this need any footage of real-looking things, or is it graphics and text?"
Quick Reference
| Task |
Tool |
| Analyze inspiration video(s) → brief + shot plan |
python scripts/analyze_reference.py --input ref.mp4 --input "https://youtube.com/..." --goal "30s 9:16 promo for X" --brand X --output refs/analysis |
| Check available Gemini models |
python scripts/list_models.py [--filter image|video] |
| Generate storyboard still (GPT Image 2) |
python scripts/generate_image_gpt.py --prompt "..." --output scene.png [--aspect 9:16] [--size 2K] [--quality high] |
| Still with character + set refs |
python scripts/generate_image_gpt.py --prompt "Image 1 = character, image 2 = room ..." --ref char.png --ref set.png --output scene.png |
| Inpaint / edit a still |
python scripts/generate_image_gpt.py --prompt "..." --edit src.png [--mask m.png] --output out.png |
| Transparent logo / overlay |
python scripts/generate_image_gpt.py --prompt "..." --aspect 1:1 --transparent --output logo.png |
| Still via Seedream 5 (BytePlus; feeds Seedance faces) |
python scripts/generate_image_byteplus.py --prompt "..." --output scene.png |
| Still via Nano Banana (fallback) |
python scripts/generate_image_gemini.py --prompt "..." [--model pro] --output scene.png |
| Video from still (Gemini Omni) |
python scripts/generate_video_omni.py --prompt "..." --image scene.png --duration 6 --output scene.mp4 |
| Omni first + last frame / subject refs |
--image start.png --last-frame end.png · --ref char.png --ref prop.png (address as <IMAGE_REF_0>) |
| Omni draft → upscale / extend / edit |
--resolution 360p then --upscale draft.mp4 --resolution 4k · --extend clip.mp4 · --edit clip.mp4 · --previous <id> |
| Video via Veo 3.1 (fallback) |
python scripts/generate_video_veo.py --prompt "..." --image scene.png --output scene.mp4 |
| Video (LTX 2.3 fast, cheapest iteration) |
python scripts/generate_video_ltx.py --prompt "..." --image scene.png --output clip.mp4 |
| Video (LTX 2.5, newest look, 6–20 s) |
python scripts/generate_video_ltx.py --ltx-model ltx-2-5-fast --prompt "..." --image scene.png --output clip.mp4 |
| LTX: extend / retake a clip (2.3 pro) |
--endpoint extend --video clip.mp4 --duration 5 · --endpoint retake --start-time 2 --duration 3 |
| LTX: audio-to-video (music-driven) |
--endpoint audio-to-video --audio music.mp3 --image scene.png |
| Video (FLUX 3: 5–20 s, dialogue + lip-sync, multi-shot in one take) |
python scripts/generate_video_flux.py --prompt "Use this image as the first frame. ..." --image scene.png --duration 8 --output clip.mp4 |
| FLUX 3: draft → enhance (⅓ cost, same seed) |
--draft --output drafts/a.mp4 then --enhance drafts/a.mp4 --resolution fhd --output clips/a.mp4 |
| FLUX 3: last frame / timed keyframes / continue / upscale |
--last-frame end.png · --mid-image b.png (evenly spaced) · --keyframe 4.5:mid.png (timed) — ≤10 images, 3+ need --duration · --video clip.mp4 (v2v, +5–15 s) · --upscale clip.mp4 --upscale-factor 2 |
| Video (Seedance 2.0, products / 3D chars) |
python scripts/generate_video_seedance.py --prompt "..." --image scene.png --output clip.mp4 |
| Video (Seedance 2.5: 30 s, 50 refs, ref videos) |
python scripts/generate_video_seedance.py --model 2.5 --duration 30 --image p.png --ref-video move.mp4 --task-type reference --prompt "... like @Video1" --output ad.mp4 |
| Seedance 2.5: edit / extend a clip |
--model 2.5 --task-type edit --ref-video clip.mp4 --prompt "Video edit: remove ..." · --task-type extend --prompt "Extend @Video1 ..." |
| Voiceover |
python scripts/generate_tts.py --text "..." --output vo.mp3 [--timestamps subs.json] |
| Music |
python scripts/generate_music.py --prompt "..." --duration 120 --output score.mp3 |
| Sound effect |
python scripts/generate_sfx.py --text "..." --duration 8 --output sfx.mp3 |
| Pick the best take per slot (Gemini) |
python scripts/pick_takes.py --slot "1920s print" takes/a.mp4 takes/b.mp4 --out picks.json |
| Quality gate — blind jury |
python scripts/jury.py renders/cut.mp4 --brief "50 s launch film for X" --runs 2 (--anchored prev.json --changes "..." only to ask what moved) |
| Phrase times from VO timestamps |
python scripts/vo_words.py audio/vo_words.json --anchor "It runs" "strategy" "every channel" |
| Measure UI boxes for overlays |
python scripts/measure_ui.py hero/screen.html --grep "INTERVIEWS" |
| Finals: conform / join / render+master / probe |
scripts/master.sh conform in.mp4 out.mp4 [1920 1080] [1.18] · master.sh join out.mp4 a.mp4:2.2 b.mp4:5.5 · master.sh render film renders/final.mp4 · master.sh probe final.mp4 |
| Spend estimate from disk |
python scripts/cost_report.py /path/to/project |
| Probe media |
python scripts/probe.py clips/ --format table |
| Assemble (ffmpeg, fast, no graphics) |
python scripts/assemble.py --videos-dir clips/ --narration vo.mp3 --music score.mp3 --output film.mp4 |
| Scaffold HyperFrames project |
scripts/new_hyperframes_project.sh /tmp/my-film/film --aspect 9:16 |
| Build timeline (index.html + scenes.json) |
python scripts/build_hyperframes_timeline.py --project /tmp/my-film/film --clip assets/clips/a.mp4:3 --title "Intro|2" --end-card "brand.com|3:cta=Shop now" --music assets/audio/bgm.mp3 --voiceover assets/audio/vo.mp3 --logo assets/images/logo.png |
| Validate composition |
(cd /tmp/my-film/film && npx --yes hyperframes@0.8.27 check) |
| Snapshot frames (no render) |
scripts/snapshot_hyperframes.sh /tmp/my-film/film 0.5,4,9 |
| Render final film |
scripts/render_hyperframes.sh /tmp/my-film/film renders/film.mp4 [--quality draft] [--format gif] |
| Prompting: images |
reference/gpt-image.md · reference/seedream.md · reference/nano-banana.md (fallback) |
| Prompting: video |
reference/omni.md · reference/flux3.md · reference/ltx.md · reference/seedance.md · reference/veo.md (fallback) |
| Voice / music / SFX |
reference/elevenlabs.md |
| Inspiration analysis guide |
reference/inspiration.md |
| HyperFrames assembly guide |
reference/hyperframes.md |
| Worked prompt examples per model + production shapes |
reference/examples.md — official Omni/Veo/Seedance/LTX examples, UGC one-shot template, asset-lock commercial, keynote choreography, ad timing rules |
| Hard-learned lessons |
reference/learnings.md |
| Worked example (50 s B2B launch film, 16:9 + 9:16) |
examples/kontentplus-intro/ — spec/overlay scripts, VO-driven timing, native product canvas, era-footage picker, jury logs, cost |
All scripts require source ~/config.env first. If not found, ask the user where it is (never guess or print key values).
Tip: keep the real keys in one gitignored config.env (e.g. inside your clone of this repo) and make ~/config.env a small loader that sources it. media_host.py also accepts the alias names S3_BUCKET_NAME, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY and ASSET_STORAGE=s3|r2; map any other aliases (e.g. BYTEPLUS_ARK_API_KEY → ARK_API_KEY, LTX_API_KEY → LTXV_API_KEY) in that loader.
Legacy Remotion template + scripts: legacy/ (kept for old projects; /remotion-to-hyperframes ports compositions).
Setup
source ~/config.env # MUST run before any API call
pip install -r requirements.txt # openai google-genai pillow requests boto3 yt-dlp
brew install ffmpeg # macOS
node --version # 22+ for HyperFrames (npx fetches hyperframes@0.8.27 on first use)
config.env must define:
OPENAI_API_KEY — OpenAI (GPT Image 2 stills)
GEMINI_API_KEY — Google AI Studio (Omni video, reference-video analysis, Nano Banana / Veo fallbacks)
ELEVENLABS_KEY, ELEVENLABS_VOICE — ElevenLabs (voice, music, SFX)
ARK_API_KEY — BytePlus Ark (Seedance 2.5/2.0, Seedream 5)
LTXV_API_KEY — LTX (Lightricks)
BFL_API_KEY — Black Forest Labs (FLUX 3 Video; https://dashboard.bfl.ai). Local files go inline as base64, no S3 needed.
S3_BUCKET, S3_REGION (+ optional S3_PUBLIC_URL, S3_PREFIX) — Amazon S3; hosts local images/videos as URLs for Seedance/Seedream (scripts/media_host.py check to verify). Standard AWS_* credentials or AWS_PROFILE.
Film Production Workflow
0. Inspiration analyze_reference.py → refs/analysis/brief.md (STYLE_LOCK, pacing, shot plan, model per shot)
1. Script script.md + scenes.md + lock blocks (character / set / props)
2. References refs/*.png (character sheet, set wide, set detail) ← approve
3. Storyboard storyboard/scene_NN.png with all refs attached ← approve (gate!)
4. Clips clips/scene_NN.mp4 (Omni / LTX / Seedance per brief) → clips/cfr/
5. Audio audio/vo.mp3 (+ subs.json), bgmusic.mp3, sfx/
6. Assemble HyperFrames project → check → snapshot → render (product beats NATIVE, see §Product sections)
7. Review watch, fix, re-render; blind jury.py as the gate (twice); stop when its notes turn structural
8. Finals master.sh: HD conform → high-quality render → −14 LUFS → faststart; 9:16 from the same build
Step 0: Inspiration & Reference Analysis (do this first)
Ask for (or find) 1–3 reference videos the user admires in the same category — a competitor ad, a brand film, a
reel. Then:
python scripts/analyze_reference.py \
--input "https://www.youtube.com/watch?v=..." --input refs/competitor.mp4 \
--goal "30s vertical product promo for NORRA face serum, Instagram Reels, no faces in close-up" \
--brand NORRA --output refs/analysis
You get per-video shot lists + contact sheets and a fused brief.md: STYLE_LOCK (paste into every prompt),
PACING_TARGET, CAMERA_VOCAB, AUDIO_DIRECTION, TYPOGRAPHY, a shot plan with a recommended model per shot and
prompt seeds, the consistency locks you need, risks, and a do-not-copy list. Show the brief + contact sheets to the
user and get sign-off before writing the script. Details: reference/inspiration.md.
No reference available? Skip to Step 1 but still write a STYLE_LOCK by hand (medium, lens, grade, light, palette).
Step 0.5: Directing gates — read reference/directing.md before Step 1
No still before the concept is locked in words, no video before the shot list is approved. Four gates: G0 brief
reflected back → G1 one concept locked (spine, but/therefore, wound/stakes/turn, ending first) → G2 timed beat
sheet with value shifts and an audience-knowledge ledger (every fact the ending needs is planted on screen earlier)
→ G3 shot list with a reason for every camera move and a 30–50-word identity string per character, pasted
verbatim into every prompt from here on. Prompts name bodies and objects, never emotions or "cinematic". When a
take fails, name the failure code and climb the cost ladder (prompt edit → parameter → regenerate → keyframe →
re-plan → fix in the edit). The vendored source skills are in vendor/ (see vendor/VENDOR.md).
The director's sheet (Gates 2–3 as one reviewable page)
Once the beat sheet exists, turn it into a plan file and a review page, and stop until the reviewer has decided
every scene. Non-directors approve better from a sheet than from a chat thread.
# plan.json: title, spine, acts, cast (identity strings, sheet images), scenes (purpose, what-the-audience-learns,
# value shift, coverage table, dialogue native+en, continuity, previs image key) — schema in scripts/director_sheet.py
python3 scripts/director_sheet.py build --plan v3/sheet/plan.json --out v3/sheet/sheet.html --images v3/previs v2/refs
- Previs: one still per scene (GPT Image 2, 1K medium, cast sheets as
--ref, the first wide shot as the prompt) so the
reviewer sees how it will appear; put them in an images dir as previs_<sceneId>.jpg.
- In Claude Code: publish
sheet.html with the Artifact tool and capabilities: {db: {}}. Approvals and notes are
stored per scene in the artifact db (collection reviews, doc id = scene id). Read them back with
read_db (db_op list, collection reviews), save as v3/sheet/decisions.json, then
python3 scripts/director_sheet.py decisions --plan … --decisions … prints the retake/rewrite list and says whether
Gate 3 is open.
- Outside Claude Code (a client, a teammate with the HTML file): the same page works from a file or any host; the
reviewer presses Copy decisions as JSON and sends it back; save that text as
decisions.json. No server needed.
- Rule: no video generation until
decisions reports every scene approved. Scenes marked needs changes go back to
the beat sheet, not to the prompt.
Step 1: Script & Scene Breakdown
Write the narration/dialogue script (≈2.5 words/s for VO). Break into scenes: | Scene | Time | Duration | Setting | Action | Camera | Mood | VO |. Durations come from the brief's PACING_TARGET (ads: 2–3.5 s per shot, hook by 2 s;
films: 4–8 s). For ads/promos add: product first-reveal time, hero moment, end card + CTA copy, logo placement.
Define lock blocks — identical text pasted into every relevant prompt:
Character Lock Block (one per recurring character):
TAKESHI (old) -- Elderly Japanese man, late 70s, thin white hair swept back,
wire-rimmed round spectacles, gentle weathered face. Worn dark brown wool vest
over cream linen shirt, leather work apron with tool pockets.
Set Lock Block (one per recurring location, with LEFT/RIGHT positions, surfaces, walls, light sources):
WORKSHOP -- Traditional Japanese clock repair workshop. Wooden workbench against
back wall, single oil lamp on the LEFT side of bench. Walls floor-to-ceiling with
clocks: 3 grandfather clocks on the LEFT wall, rows of pendulum clocks above the bench,
pocket watches on hooks to the RIGHT. Window on the LEFT wall. Scattered brass gears,
tweezers, magnifying loupe, small brass bird near the RIGHT edge of the bench.
Product Lock Block (ads): exact object, materials, finish, label text, what must NOT change, and the framing rule
from the brief (e.g. "product at 10–15 % of frame, never macro on the pump/logo").
Step 2: Reference Images
# Character sheet (1:1, clean background)
python scripts/generate_image_gpt.py --prompt "Character reference sheet. Front-facing portrait of: [CHARACTER LOCK]. Neutral background." --output refs/character.png --aspect 1:1 --quality high
# Set wide + set detail
python scripts/generate_image_gpt.py --prompt "[STYLE_LOCK] [SET LOCK]. Wide establishing shot of the full room. Set reference — every object position must be remembered." --output refs/set_wide.png
python scripts/generate_image_gpt.py --prompt "[STYLE_LOCK] Close-up of the workbench surface from [SET LOCK]. Prop reference." --output refs/set_detail.png
# Product: prefer the brand-approved still; if regenerating, --edit product.png (+ --mask) preserves geometry; GPT Image 2 renders label text reliably (quote it)
Review and approve ALL references before generating any scene stills.
Step 3: Storyboard Stills
One still per scene, passing every relevant ref (character + set + product, up to 4):
python scripts/generate_image_gpt.py \
--prompt "Image 1 is the character (keep the face identical), image 2 is the set (keep the layout). [STYLE_LOCK] [CHARACTER LOCK] [SET LOCK] [SCENE: shot size, camera, action, light]" \
--ref refs/character.png --ref refs/set_wide.png \
--output storyboard/scene_02.png --aspect 9:16 --quality medium
Generate 3–4 in parallel (background processes). Build a contact sheet and show the user:
ffmpeg -y -i storyboard/scene_%02d.png -filter_complex "scale=480:-1,tile=4x3:margin=4:padding=4" storyboard/contact_sheet.png
Consistency checklist: same face / same room layout / same props / same light direction / same palette across scenes.
Do not generate video until ALL stills are approved. (Stills cost cents; clips cost dollars.)
Step 4: Video Clips
Pick the model per shot (the brief already suggests one):
| Feature |
Gemini Omni 1.1 Flash |
FLUX 3 Video |
Seedance 2.5 |
Seedance 2.0 |
LTX 2.5 |
LTX 2.3 |
| Provider |
Google (generate_video_omni.py) |
Black Forest Labs (generate_video_flux.py) |
BytePlus Ark (--model 2.5) |
BytePlus Ark (--model full/fast/mini) |
LTX (--ltx-model ltx-2-5-*) |
LTX (--ltx-model ltx-2-3-*) |
| Photoreal human faces |
✅ |
✅ |
❌ blocked (Seedream 5 workaround) |
❌ blocked |
✅ |
✅ |
| Duration |
3–10 s (+extend to 40 s) |
5–20 s (+5–15 s per continuation) |
4–30 s |
4–15 s |
6–20 s (fast) / 6–10 s (pro) |
2–20 s |
| Max resolution |
4K (upscaled from 720p) |
1080p (fhd upsampler) · --upscale ×3 |
1080p 10-bit |
4K |
4K (fast) |
4K |
| First + last frame |
✅ |
✅ + up to 10 timed keyframes |
✅ |
✅ |
✅ |
✅ |
| Reference videos (motion/camera) |
✅ 3 × ≤3 s |
❌ (continuation only) |
✅ up to 10 |
✅ 3 |
❌ |
❌ |
| Extend / edit |
✅ extend + conversational edit + --previous |
continue (v2v) · draft → enhance (same seed) · no edit |
edit + extend |
edit + extend |
❌ |
retake/extend (pro) |
| Multi-shot in one generation |
one continuous shot |
✅ SHOT N: … HARD CUT. |
time-coded beats |
time-coded beats |
2–3, less reliable |
2–3, less reliable |
| Audio |
built-in |
built-in, dialogue + lip-sync 13+ languages |
generate_audio |
generate_audio |
built-in |
built-in |
| Cost / 8 s (approx.) |
$0.24 (360p) · $0.80 (720p) · $1.20 (1080p) |
$0.48 (draft) · $1.36 (hd) · $2.32 (fhd) |
~$1.2 |
~$1.0 |
~$0.7–1.2 |
~$0.25 |
| Best for |
hero shots with people, 360p drafts → 4K, fixing a take by talking to it |
dialogue scenes, multi-shot sequences, era/doc formats, storyboard keyframes |
30 s one-shot ads, "camera like @Video1", 3D chars, products |
products, motion refs |
newest look, long takes |
cheap iteration, faces, retakes |
Veo 3.1 is still wired up as a fallback (generate_video_veo.py, see reference/veo.md). Routing rule of thumb: a shot
with talking or more than one angle → FLUX 3; a hero shot you will iterate by feel → Omni; a 30 s product one-take with a motion
reference → Seedance 2.5; coverage at scale or a fix inside a clip → LTX. Details: reference/flux3.md.
# Most scenes: image-to-video, prompt = ONLY [CAMERA] + [ACTION] + [LIGHT CHANGE] + [AUDIO]; the still carries the look
python scripts/generate_video_ltx.py --prompt "Camera holds. The rim light breathes brighter over six seconds. Quiet room tone." --image storyboard/scene_01.png --output clips/scene_01.mp4
python scripts/generate_video_omni.py --prompt "In a single continuous shot, slow dolly forward. Ambient: crickets, water. No music." --image storyboard/scene_02.png --duration 6 --output clips/scene_02.mp4
# FLUX 3: dialogue scene from a still — draft at ⅓ cost, then enhance the keeper (same generation, no re-roll)
python scripts/generate_video_flux.py --prompt "Use this image as the first frame. Camera locked at eye level, 70 mm, shallow focus. After a beat she looks up and says, quiet and warm, 'Try it now.' Ambience: rain on the window, a clock tick. No music, no on-screen text, no subtitles." --image storyboard/scene_04.png --duration 8 --draft --output drafts/scene_04.mp4
python scripts/generate_video_flux.py --enhance drafts/scene_04.mp4 --resolution fhd --output clips/scene_04.mp4
# Omni draft loop: 360p first, refine by talking to it, then upscale the keeper
python scripts/generate_video_omni.py --prompt "..." --image storyboard/scene_03.png --resolution 360p --output drafts/scene_03.mp4
python scripts/generate_video_omni.py --previous <id from the run above> --prompt "Slower camera, keep everything else the same" --resolution 360p --output drafts/scene_03_v2.mp4
python scripts/generate_video_omni.py --upscale drafts/scene_03_v2.mp4 --resolution 1080p --output clips/scene_03.mp4
# Seedance 2.5: whole 30 s ad in one shot with the inspiration's camera move as a reference video
python scripts/generate_video_seedance.py --model 2.5 --duration 30 --ratio 9:16 --task-type reference \
--image refs/product.png --ref-video refs/analysis/_cuts/lateral_track.mp4 \
--prompt "[STYLE_LOCK] Product like @Image1, camera move like @Video1. 0-3s ... 3-8s ... " --output clips/ad_oneshot.mp4
Then convert every clip to CFR and trim to the best 2–3.5 s for ads (AI clips are strongest in their first seconds):
mkdir -p clips/cfr && for f in clips/*.mp4; do ffmpeg -y -i "$f" -r 24 -vsync cfr -c:v libx264 -preset fast -crf 18 -c:a aac "clips/cfr/$(basename "$f")"; done
Review each clip (thumbnail strip): face/product match, camera executed, no jitter, physics OK. Regenerate failures.
Step 5: Audio
python scripts/generate_tts.py --file script.txt --output audio/vo.mp3 --speed 0.95 --timestamps audio/vo_subs.json
python scripts/generate_music.py --prompt "[AUDIO_DIRECTION from brief — style only, no artist names]" --duration 40 --output audio/bgmusic.mp3
python scripts/generate_sfx.py --text "glass pipette click, viscous drop" --duration 3 --output audio/sfx/drop.mp3
# Subtitles from the FINAL vo file (never reuse old timestamps):
whisper audio/vo.mp3 --model base --output_format json --word_timestamps True --output_dir audio/
Make the music at least as long as the film (the builder warns if not).
Step 6: Assemble with HyperFrames
scripts/new_hyperframes_project.sh /tmp/my-film/film --aspect 9:16
cp clips/cfr/*.mp4 /tmp/my-film/film/assets/clips/ && cp audio/*.mp3 /tmp/my-film/film/assets/audio/ && cp refs/logo.png /tmp/my-film/film/assets/images/
python scripts/build_hyperframes_timeline.py --project /tmp/my-film/film \
--title "NORRA|1.5:sub=Skin, simplified" \
--clip "assets/clips/scene_01.mp4:3:text=Made for mornings" \
--clip assets/clips/scene_02.mp4:2.5:trim=1.0 \
--clip assets/clips/scene_03.mp4:3:audio=1 \
--end-card "norra.se|3:cta=Shop the serum" \
--music assets/audio/bgmusic.mp3 --music-volume 0.15 \
--voiceover assets/audio/vo.mp3 --vo-offset 0.8 --subtitles audio/vo.json \
--logo assets/images/logo.png --transition crossfade --transition-duration 0.4 --aspect 9:16 --fps 24
(cd /tmp/my-film/film && npx --yes hyperframes@0.8.27 check) # lint + runtime + layout + motion + contrast
scripts/snapshot_hyperframes.sh /tmp/my-film/film 0.5,3,6,9 # look at PNGs before paying for a render
scripts/render_hyperframes.sh /tmp/my-film/film renders/draft.mp4 --quality draft
scripts/render_hyperframes.sh /tmp/my-film/film renders/film.mp4 # final
scenes.json next to index.html is the editable timeline (reorder, retime, add "text", swap "transition"), then
--spec scenes.json. For anything the generator doesn't do (kinetic type, lower-thirds, shader transitions,
audio ducking, sub-compositions), hand-edit index.html following reference/hyperframes.md
or invoke the official /hyperframes skills (installed globally). Quick, graphics-free cuts: scripts/assemble.py (ffmpeg).
Step 7: Review & Iterate
Watch the render. Fix by layer: pacing → retime scenes.json; missing beat → generate/extend a clip (LTX extend,
Seedance 2.5 extend, FLUX 3 --video continuation); character/set drift → regenerate the still with the refs; audio → volumes in scenes.json; text →
edit index.html. Re-run check, snapshot the changed timestamps, re-render draft, then final.
Product sections — build them natively, never as screenshots
Screenshots of an app with highlight rings read as slop and two rounds of them were rejected. What works:
- A title scene with empty text that your overlay script fills with real HTML: an agent log that checks off, a real
post card with a generated photo, channel chips lighting up, a chart drawing with counters, a proposal being approved
(blueprints:
agent-progress-theater, grid-card-assemble, constellation-hub, dataviz-countup in
/hyperframes-animation). One oversized world inside a camera wrapper (transform-origin: 0 0; to centre world
point (wx, wy) at scale s: x = FW/2 − wx·s, y = FH/2 − wy·s) and pan between stations on the spoken words.
- Set
text-align: left; line-height: 1.25 on the injected root (title cards centre everything) and use the real logo
mark in cards, never a coloured square.
- If a real screenshot plate must appear, measure its boxes (
measure_ui.py) and map css → frame
(x·4/3, y·4/3 − 60 for a 1440×900 plate covered into 16:9); guessed ring coordinates were wrong three times out of three.
- Live-action cutaways after the interface break the "motion-first" flow for evaluators; if one is needed, bake the real
product chart onto its screen (
generate_image_gpt.py --edit still.png --ref chart.png) before animating it.
Word-synced editing
Every era push, kinetic card and camera move lands on a spoken word, not on a guess:
generate_tts.py --timestamps audio/vo_words.json (character timestamps).
- Schedule VO lines sequentially with a fixed gap; size each scene so the next anchored scene starts at
line.start − offset (+ the transition overlap), clamped to the source length. Era offsets of −0.05 s make the push
land on the line, not before it.
vo_words.py --anchor "<line start>" "<phrase>" gives the offset of a phrase inside a line → key tweens to
line.start + offset. Long slots: join two takes with a hard cut so each phrase has its picture
(master.sh join); slow a short take with setpts (1.18× is invisible on hand-cranked footage).
- Music "hard stop" requests are ignored by the generator — build the stop in the mix (trim + silence + sub pulse +
looped calm section).
Archival / era footage that reads as real
GPT Image 2 still → Omni image-to-video, not text-to-video:
- Still prompt as an authentic archival photograph: name the process (glass plate, silver gelatin, Kodachrome, 35 mm
consumer film + flash, camcorder), period clothing, real faces (Omni accepts them; Seedance rejects them in inputs),
plate damage/grain/vignette, "no readable text, no modern objects".
- Omni prompt = era artefacts + natural, moderate, continuous motion in one locked-off setup: "no one enters or
leaves, no new figures, hands keep their shape, no pans, no cuts". "Hold still" direction scores lower (frozen).
Known failures: phantom figures after ~4 s (use the first 3 s), a whip-pan cut when two actions are listed, floating
sci-fi UI when notifications are mentioned near a face, garbled text on generated screens.
- Two takes per slot →
pick_takes.py (authenticity / motion / era / usability + artefact timestamps + best in-point).
- 720p drafts; finals via
--upscale (the script refines the original interaction, which works from the EEA and is a
true upscale). Conform with master.sh conform (crf 14, lanczos, 24 fps CFR).
Quality gate — how to use the jury honestly
- Run
jury.py blind, twice, with the explicit scale. Feeding it previous scores anchors it (it returned 7/10 for six
rounds while calling every fix "improved"); use --anchored only to ask what moved.
- Blind Gemini tops out around 6–7 for generated plates; when its notes turn structural (a live-action cutaway
after the UI, the tone at the turn) rather than about detail, more generations will not move it — say so and ship.
- Act on notes in this order: sync (pushes on lines) → product clarity (native beats) → B2B proof (pipeline, CPL,
hours saved, an approve click) → end card (mark assembles, high-contrast CTA, risk-reversal line) → footage.
Finals, masters and aspect variants
master.sh render <project> renders/final.mp4: high-quality render → loudnorm I=-14 TP=-1.5 LRA=11 → AAC 256k →
+faststart; keep the _raw next to it. master.sh probe reports dims, fps, LUFS, true peak.
- Write overlays with
FW/FH and helper cx/cy from day one; a ASPECT=9:16 OUT_DIR=../film_916 switch (module
60..1020 × 520..1060, axis at 1180, pushes ±FW, UI plates as cards with their own mapping, live action inside a
module) turns the same build into the vertical cut. After any templating pass run
grep -o 'top:{F[HW][^"]*' index.html — an unevaluated brace silently drops the element onto the class position.
- Cost:
cost_report.py <project> before the client asks. A 50 s film with two footage rounds ran ≈ $65 in API spend
(Seedance ≈ $30, Omni ≈ $22, stills ≈ $8, audio ≈ $4).
- Ask for the tagline and real metrics at intake; placeholders survive to the master otherwise.
Film Types — what changes
|
Short film |
Ad / commercial |
Product promo / demo |
Social reel |
| Length |
60–180 s |
6 / 15 / 30 s |
20–60 s |
7–30 s |
| Aspect |
16:9 |
16:9 + 9:16 cutdown |
16:9 or 9:16 |
9:16 |
| Shot length |
4–8 s |
2–3.5 s, hook ≤ 2 s |
3–5 s |
1.5–3 s |
| Step 0 refs |
1 film in the genre |
2–3 ads in the category |
competitor demo + one brand film |
3 top reels |
| Must-have beats |
arc: setup → tension → climax → resolution |
hook, product reveal, hero moment, end card + CTA + logo |
problem → product → proof → CTA |
hook in first frame, caption-first, loopable end |
| Faces |
Omni / FLUX 3 / LTX |
often none (product) → Seedance 2.5 shines |
hands only → Seedance / LTX |
UGC feel → LTX + documentary prompt stack |
| Typography |
title + credits |
headline overlays, price/claim, CTA pill |
feature callouts |
burned-in captions (--subtitles) |
| Audio |
VO + score + SFX |
music-led, 1–2 VO lines, SFX hits on cuts |
VO-led, light bed |
music + captions, VO optional |
| Assembly |
--transition blur/dip, fades |
--transition cut/crossfade 0.3, --end-card ...:cta= |
--clip ...:text= callouts |
--fps 30, --aspect 9:16, --fade-out 0 for loops |
Ads: also render the 16:9 master and a 9:16 cutdown (--aspect 9:16 with re-framed stills or --resolution),
and a 6 s bumper from the hero shot + end card.
Visual Consistency Rules
Three layers, each enforced by a reference image and a lock block in every prompt:
| Layer |
What |
How |
| Character |
face, hair, clothes, build |
character sheet --ref + Character Lock Block |
| Set / location |
layout, walls, furniture, light direction, palette |
set wide/detail --ref + Set Lock Block |
| Props / product |
objects don't teleport; product geometry/label intact |
detail --ref + Product Lock Block; no macro on hardware/logos; typography in post |
Reference images beat prompt tokens: with strong refs the video prompt is 30–80 words of pure camera/action/audio. See
reference/learnings.md for the failure modes (LTX particle triggers, Seedance photoreal
threshold, Nano Banana geometry drift) and fixes.
Prompting Cheat Sheet
- Images (GPT Image 2):
[Subject + adjectives] + [Action] + [Location] + [Composition/Camera] + [Light] + [STYLE_LOCK] — full sentences, brief an art director. Quote any on-image text (the label reads "NORRA"). Name what each --ref is for. See reference/gpt-image.md.
- Gemini Omni: "In a single continuous shot, [camera rig + move]. [What changes, with timing: 'after 3 seconds ...']. [Light]. Audio: [what you want] — no music / no dialogue if unwanted." Refs as
<IMAGE_REF_n>. Edits: short + "Keep everything else the same." See reference/omni.md.
- In-model effects vs. post: ask a video model for diegetic effects only (smoke, light, particles, physical transformations). Text, logos, numbers and UI always go through HyperFrames (see Intake).
- LTX 2.3 / 2.5: one flowing present-tense paragraph; i2v = describe the change, not the subject; never mention particles/dust/fluids unless wanted; "Camera holds." for static.
- FLUX 3: brief a colleague in prose — it rewrites the prompt and fills every layer you leave unnamed. Name camera + subject/action + light + motion quality + each audio layer (speech in quotes with a visible speaker, ambience, effects, music or "no music") and end with "no on-screen text, no subtitles". i2v opens "Use this image as the first frame." + what to leave alone. Multi-shot:
SHOT ONE: … HARD CUT. SHOT TWO: …, ~5 s per shot, adjacent shots must contrast. Continuation opens "Continue the reference video from its final frames." Draft first. See reference/flux3.md.
- Seedance 2.5 / 2.0:
[Style preamble] + [Lock blocks] + [Scene] + time-coded beats (0-3s / 3-6s ...) + [Light/mood] + [Transition hint]; name every reference's role (@Image1 = product, @Video1 = camera move). For 30 s one-shots write 6–8 time-coded beats incl. the end card. See /seedance-prompt.
- Lock blocks go into EVERY prompt for that character/set/product, verbatim.
- One job per reference (Seedance): "@Image 1 defines the product geometry only. Ignore the background." Conflicts resolve identity → prop → wardrobe → location → light → camera.
- Say the sound (LTX/Omni/Veo): anything you leave out gets invented; attach every sound to something in frame; "no music" when you score in post.
- Worked, sourced examples for each of these: reference/examples.md.
Error Recovery
| Problem |
Fix |
| Model not found |
python scripts/list_models.py — names change; LTX ltx-2-* ids retired Aug 2026; Omni = gemini-omni-1.1-flash; GPT Image = gpt-image-2 |
Omni: no video in response / HTTP 4xx |
check the prompt for policy triggers; editing/extending uploaded video is unavailable in EEA/CH/UK; recover a finished output with --download <interaction_id> |
Omni --upscale → "Exactly one input video is required for edit task" |
uploaded-video edits are blocked in EEA/CH/UK; the script now refines the original interaction from <clip>.mp4.json (--previous) — keep sidecars next to clips |
HyperFrames gsap_exit_missing_hard_kill |
a fade ends within ~0.1 s of the next clip's data-start — move it earlier or add a tl.set |
| Overlay element sits in the wrong place after a templating pass |
an unevaluated {FH …} in a plain string → invalid CSS → class position wins; grep -o 'top:{F[HW]' index.html |
| Highlight rings / cursor off the UI element |
never guess: measure_ui.py and map css → frame |
| Omni clip too short / long |
there is no duration field — --duration N writes it into the prompt; chain --extend for > 10 s |
| GPT Image 2 rate limit (5 img/min tier 1) |
generate ≤4 stills in parallel; use --quality medium for coverage |
Seedance InputImageSensitiveContentDetected |
photoreal face → use Omni/LTX, or Seedream 5 text-only still → Seedance (reference/seedance.md) |
media_host: no hosting configured / S3 upload failed |
set S3_BUCKET + S3_REGION in ~/config.env, run python scripts/media_host.py check; without S3_PUBLIC_URL the script hands Ark a 24 h presigned URL (bucket can stay private) |
Seedance InvalidParameter.TaskTypeConstraint/Mismatch |
edit/extend/first-frame need ratio=adaptive (script forces), edit needs duration=-1 and edit/remove/replace words in the prompt |
| Character / set drift |
same --ref images every time + lock blocks; extract a clean frame from the first good clip as the new character ref |
| Props appear/disappear |
put all props in the Set Lock Block from scene 1 |
| Video jitter / freeze at end of a scene |
CFR convert; slot longer than source → shorten --clip seconds or LTX extend |
hyperframes lint errors |
read the message — usually a <video> inside a timed wrapper, a missing audio id, or crossorigin; see reference/hyperframes.md |
check contrast failure on overlay text |
keep the text-shadow, add a scrim, or move the overlay off bright footage |
| Music shorter than film |
regenerate with --duration ≥ film length; the builder clamps and warns |
| Subtitles out of sync |
re-run whisper on the final vo file; check --vo-offset matches |
| ElevenLabs music TOS error |
remove artist/brand names |
| Gemini video analysis fails on URL |
non-YouTube URLs are downloaded with y |
…(truncated)
1---2name: ai-film-studio3description: AI film production studio with pre-built scripts: analyze inspiration/reference videos (Gemini), write script + storyboard, generate stills (GPT Image 2; Seedream 5 / Nano Banana as fallbacks), video (Gemini Omni 1.1 Flash, FLUX 3 Video, Seedance 2.5/2.0, LTX 2.5/2.3; Veo 3.1 fallback), audio (ElevenLabs), and assemble the final cut with HyperFrames (HTML→MP4; replaced Remotion) or ffmpeg. Use when: creating short films, ads, product promos, brand films, social reels, storyboards, video scenes, voiceovers, music, sound effects, or any AI-powered media creation — especially "make one like this reference video". Trigger words: film, movie, video, ad, commercial, promo, product video, reel, storyboard, animate, scene, voiceover, narration, soundtrack, clip, short film, inspiration video, reference video.4---56# AI Film Studio78End-to-end AI film production. The agent handles the creative decisions (reference analysis, story, prompts, pacing);9pre-built scripts handle the API plumbing. Works for **short films, ads, product promos, brand films and social reels** —10the pipeline is the same, only Step 0 (inspiration) and the assembly template (end card, CTA, aspect) change.1112## Intake — route before you generate anything1314Classify the deliverable first. Only the last two rows need generative video models; everything else is HyperFrames alone.1516| The user wants… | Route | Why |17|---|---|---|18| **Motion graphics only** — kinetic type, logo sting, stat count-up, chart, lower-third, animated headline/tweet, transparent overlay (usually < 15 s, no footage) | `/motion-graphics` (official HyperFrames workflow skill; if missing: `npx hyperframes@0.8.27 skills update motion-graphics`) | zero generation cost; deterministic, editable HTML |19| A promo/site tour from a **website URL** | `/product-launch-video` | captures the real site; no AI footage needed |20| A narrated **explainer from text/notes**, no URL | `/faceless-explainer` | HyperFrames generates its own visuals |21| **Existing footage** that needs captions or graphic packaging | `/embedded-captions` (subtitles) · `/talking-head-recut` (titles, lower-thirds, callouts) | footage untouched |22| A **deck / presentation** | `/slideshow` | navigable deck, not a render |23| **Beat-synced cut of images/clips to a music track** | `/music-to-video` | beat grid drives the edit |24| **Film / ad / promo / reel with generated footage** | this skill, Steps 0–7 | needs stills + video models |25| **Hybrid**: generated plates *plus* heavy graphics (kinetic type over AI footage, data callouts, shader transitions) | Steps 0–5 here for the clips, then `/hyperframes-animation` blueprints + `/hyperframes-core` instead of `build_hyperframes_timeline.py` | the simple timeline builder only does cards, overlays, captions and crossfades |2627Rules of thumb:28- If no shot needs *photographed-looking* content, do not open a video model. Type, shapes, charts and logos are HyperFrames' job.29- **Diegetic vs. graphic.** Omni/Seedance may render effects that live *inside* the footage (smoke forming a shape, light writing a word, particles becoming a product). Anything that must be exact and editable — brand typography, prices, claims, CTAs, captions, lower-thirds — is composited in HyperFrames, never generated. Video-model text drifts frame to frame and cannot be fixed afterwards.30- Workflow skills (`motion-graphics`, `product-launch-video`, `faceless-explainer`, `embedded-captions`, `talking-head-recut`, `slideshow`, `music-to-video`, `general-video`, `media-use`) install **on demand**: `npx hyperframes@0.8.27 skills update <name …>` (the core `hyperframes-*` set alone does not include them). Check with `ls ~/.claude/skills`.31- When routing to an official HyperFrames skill, hand over the brief (product, duration, aspect, brand colours/fonts, any `STYLE_LOCK` from Step 0) and let that skill run its own loop (`init → lint → check → preview → render`). Come back here only if it turns out real footage is needed.32- Unsure? Ask one question: *"Does this need any footage of real-looking things, or is it graphics and text?"*3334## Quick Reference3536| Task | Tool |37|------|------|38| **Analyze inspiration video(s) → brief + shot plan** | `python scripts/analyze_reference.py --input ref.mp4 --input "https://youtube.com/..." --goal "30s 9:16 promo for X" --brand X --output refs/analysis` |39| Check available Gemini models | `python scripts/list_models.py [--filter image\|video]` |40| **Generate storyboard still (GPT Image 2)** | `python scripts/generate_image_gpt.py --prompt "..." --output scene.png [--aspect 9:16] [--size 2K] [--quality high]` |41| Still with character + set refs | `python scripts/generate_image_gpt.py --prompt "Image 1 = character, image 2 = room ..." --ref char.png --ref set.png --output scene.png` |42| Inpaint / edit a still | `python scripts/generate_image_gpt.py --prompt "..." --edit src.png [--mask m.png] --output out.png` |43| Transparent logo / overlay | `python scripts/generate_image_gpt.py --prompt "..." --aspect 1:1 --transparent --output logo.png` |44| Still via Seedream 5 (BytePlus; feeds Seedance faces) | `python scripts/generate_image_byteplus.py --prompt "..." --output scene.png` |45| Still via Nano Banana (fallback) | `python scripts/generate_image_gemini.py --prompt "..." [--model pro] --output scene.png` |46| **Video from still (Gemini Omni)** | `python scripts/generate_video_omni.py --prompt "..." --image scene.png --duration 6 --output scene.mp4` |47| Omni first + last frame / subject refs | `--image start.png --last-frame end.png` · `--ref char.png --ref prop.png` (address as `<IMAGE_REF_0>`) |48| Omni draft → upscale / extend / edit | `--resolution 360p` then `--upscale draft.mp4 --resolution 4k` · `--extend clip.mp4` · `--edit clip.mp4` · `--previous <id>` |49| Video via Veo 3.1 (fallback) | `python scripts/generate_video_veo.py --prompt "..." --image scene.png --output scene.mp4` |50| Video (LTX 2.3 fast, cheapest iteration) | `python scripts/generate_video_ltx.py --prompt "..." --image scene.png --output clip.mp4` |51| Video (LTX 2.5, newest look, 6–20 s) | `python scripts/generate_video_ltx.py --ltx-model ltx-2-5-fast --prompt "..." --image scene.png --output clip.mp4` |52| LTX: extend / retake a clip (2.3 pro) | `--endpoint extend --video clip.mp4 --duration 5` · `--endpoint retake --start-time 2 --duration 3` |53| LTX: audio-to-video (music-driven) | `--endpoint audio-to-video --audio music.mp3 --image scene.png` |54| **Video (FLUX 3: 5–20 s, dialogue + lip-sync, multi-shot in one take)** | `python scripts/generate_video_flux.py --prompt "Use this image as the first frame. ..." --image scene.png --duration 8 --output clip.mp4` |55| FLUX 3: draft → enhance (⅓ cost, same seed) | `--draft --output drafts/a.mp4` then `--enhance drafts/a.mp4 --resolution fhd --output clips/a.mp4` |56| FLUX 3: last frame / timed keyframes / continue / upscale | `--last-frame end.png` · `--mid-image b.png` (evenly spaced) · `--keyframe 4.5:mid.png` (timed) — ≤10 images, 3+ need `--duration` · `--video clip.mp4` (v2v, +5–15 s) · `--upscale clip.mp4 --upscale-factor 2` |57| Video (Seedance 2.0, products / 3D chars) | `python scripts/generate_video_seedance.py --prompt "..." --image scene.png --output clip.mp4` |58| **Video (Seedance 2.5: 30 s, 50 refs, ref videos)** | `python scripts/generate_video_seedance.py --model 2.5 --duration 30 --image p.png --ref-video move.mp4 --task-type reference --prompt "... like @Video1" --output ad.mp4` |59| Seedance 2.5: edit / extend a clip | `--model 2.5 --task-type edit --ref-video clip.mp4 --prompt "Video edit: remove ..."` · `--task-type extend --prompt "Extend @Video1 ..."` |60| Voiceover | `python scripts/generate_tts.py --text "..." --output vo.mp3 [--timestamps subs.json]` |61| Music | `python scripts/generate_music.py --prompt "..." --duration 120 --output score.mp3` |62| Sound effect | `python scripts/generate_sfx.py --text "..." --duration 8 --output sfx.mp3` |63| **Pick the best take per slot (Gemini)** | `python scripts/pick_takes.py --slot "1920s print" takes/a.mp4 takes/b.mp4 --out picks.json` |64| **Quality gate — blind jury** | `python scripts/jury.py renders/cut.mp4 --brief "50 s launch film for X" --runs 2` (`--anchored prev.json --changes "..."` only to ask what moved) |65| Phrase times from VO timestamps | `python scripts/vo_words.py audio/vo_words.json --anchor "It runs" "strategy" "every channel"` |66| Measure UI boxes for overlays | `python scripts/measure_ui.py hero/screen.html --grep "INTERVIEWS"` |67| **Finals: conform / join / render+master / probe** | `scripts/master.sh conform in.mp4 out.mp4 [1920 1080] [1.18]` · `master.sh join out.mp4 a.mp4:2.2 b.mp4:5.5` · `master.sh render film renders/final.mp4` · `master.sh probe final.mp4` |68| Spend estimate from disk | `python scripts/cost_report.py /path/to/project` |69| Probe media | `python scripts/probe.py clips/ --format table` |70| Assemble (ffmpeg, fast, no graphics) | `python scripts/assemble.py --videos-dir clips/ --narration vo.mp3 --music score.mp3 --output film.mp4` |71| **Scaffold HyperFrames project** | `scripts/new_hyperframes_project.sh /tmp/my-film/film --aspect 9:16` |72| **Build timeline (index.html + scenes.json)** | `python scripts/build_hyperframes_timeline.py --project /tmp/my-film/film --clip assets/clips/a.mp4:3 --title "Intro\|2" --end-card "brand.com\|3:cta=Shop now" --music assets/audio/bgm.mp3 --voiceover assets/audio/vo.mp3 --logo assets/images/logo.png` |73| Validate composition | `(cd /tmp/my-film/film && npx --yes hyperframes@0.8.27 check)` |74| Snapshot frames (no render) | `scripts/snapshot_hyperframes.sh /tmp/my-film/film 0.5,4,9` |75| **Render final film** | `scripts/render_hyperframes.sh /tmp/my-film/film renders/film.mp4 [--quality draft] [--format gif]` |76| Prompting: images | [reference/gpt-image.md](reference/gpt-image.md) · [reference/seedream.md](reference/seedream.md) · [reference/nano-banana.md](reference/nano-banana.md) (fallback) |77| Prompting: video | [reference/omni.md](reference/omni.md) · [reference/flux3.md](reference/flux3.md) · [reference/ltx.md](reference/ltx.md) · [reference/seedance.md](reference/seedance.md) · [reference/veo.md](reference/veo.md) (fallback) |78| Voice / music / SFX | [reference/elevenlabs.md](reference/elevenlabs.md) |79| **Inspiration analysis guide** | [reference/inspiration.md](reference/inspiration.md) |80| **HyperFrames assembly guide** | [reference/hyperframes.md](reference/hyperframes.md) |81| **Worked prompt examples per model + production shapes** | [reference/examples.md](reference/examples.md) — official Omni/Veo/Seedance/LTX examples, UGC one-shot template, asset-lock commercial, keynote choreography, ad timing rules |82| Hard-learned lessons | [reference/learnings.md](reference/learnings.md) |83| **Worked example (50 s B2B launch film, 16:9 + 9:16)** | [examples/kontentplus-intro/](examples/kontentplus-intro/README.md) — spec/overlay scripts, VO-driven timing, native product canvas, era-footage picker, jury logs, cost |8485**All scripts require `source ~/config.env` first.** If not found, ask the user where it is (never guess or print key values).86Tip: keep the real keys in one gitignored `config.env` (e.g. inside your clone of this repo) and make `~/config.env` a small loader that sources it. `media_host.py` also accepts the alias names `S3_BUCKET_NAME`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` and `ASSET_STORAGE=s3|r2`; map any other aliases (e.g. `BYTEPLUS_ARK_API_KEY` → `ARK_API_KEY`, `LTX_API_KEY` → `LTXV_API_KEY`) in that loader.8788Legacy Remotion template + scripts: `legacy/` (kept for old projects; `/remotion-to-hyperframes` ports compositions).8990---9192## Setup9394```bash95source ~/config.env # MUST run before any API call96pip install -r requirements.txt # openai google-genai pillow requests boto3 yt-dlp97brew install ffmpeg # macOS98node --version # 22+ for HyperFrames (npx fetches hyperframes@0.8.27 on first use)99```100101`config.env` must define:102- `OPENAI_API_KEY` — OpenAI (GPT Image 2 stills)103- `GEMINI_API_KEY` — Google AI Studio (Omni video, **reference-video analysis**, Nano Banana / Veo fallbacks)104- `ELEVENLABS_KEY`, `ELEVENLABS_VOICE` — ElevenLabs (voice, music, SFX)105- `ARK_API_KEY` — BytePlus Ark (Seedance 2.5/2.0, Seedream 5)106- `LTXV_API_KEY` — LTX (Lightricks)107- `BFL_API_KEY` — Black Forest Labs (FLUX 3 Video; https://dashboard.bfl.ai). Local files go inline as base64, no S3 needed.108- `S3_BUCKET`, `S3_REGION` (+ optional `S3_PUBLIC_URL`, `S3_PREFIX`) — Amazon S3; hosts local images/videos as URLs for Seedance/Seedream (`scripts/media_host.py check` to verify). Standard `AWS_*` credentials or `AWS_PROFILE`.109110---111112## Film Production Workflow113114```1150. Inspiration analyze_reference.py → refs/analysis/brief.md (STYLE_LOCK, pacing, shot plan, model per shot)1161. Script script.md + scenes.md + lock blocks (character / set / props)1172. References refs/*.png (character sheet, set wide, set detail) ← approve1183. Storyboard storyboard/scene_NN.png with all refs attached ← approve (gate!)1194. Clips clips/scene_NN.mp4 (Omni / LTX / Seedance per brief) → clips/cfr/1205. Audio audio/vo.mp3 (+ subs.json), bgmusic.mp3, sfx/1216. Assemble HyperFrames project → check → snapshot → render (product beats NATIVE, see §Product sections)1227. Review watch, fix, re-render; blind jury.py as the gate (twice); stop when its notes turn structural1238. Finals master.sh: HD conform → high-quality render → −14 LUFS → faststart; 9:16 from the same build124```125126### Step 0: Inspiration & Reference Analysis (do this first)127128Ask for (or find) 1–3 reference videos the user admires in the same category — a competitor ad, a brand film, a129reel. Then:130131```bash132python scripts/analyze_reference.py \133 --input "https://www.youtube.com/watch?v=..." --input refs/competitor.mp4 \134 --goal "30s vertical product promo for NORRA face serum, Instagram Reels, no faces in close-up" \135 --brand NORRA --output refs/analysis136```137138You get per-video shot lists + contact sheets and a fused **`brief.md`**: STYLE_LOCK (paste into every prompt),139PACING_TARGET, CAMERA_VOCAB, AUDIO_DIRECTION, TYPOGRAPHY, a **shot plan with a recommended model per shot** and140prompt seeds, the consistency locks you need, risks, and a do-not-copy list. Show the brief + contact sheets to the141user and get sign-off before writing the script. Details: [reference/inspiration.md](reference/inspiration.md).142143No reference available? Skip to Step 1 but still write a STYLE_LOCK by hand (medium, lens, grade, light, palette).144145### Step 0.5: Directing gates — read `reference/directing.md` before Step 1146147No still before the concept is locked in words, no video before the shot list is approved. Four gates: **G0** brief148reflected back → **G1** one concept locked (spine, but/therefore, wound/stakes/turn, ending first) → **G2** timed beat149sheet with value shifts and an *audience-knowledge ledger* (every fact the ending needs is planted on screen earlier)150→ **G3** shot list with a reason for every camera move and a 30–50-word **identity string** per character, pasted151verbatim into every prompt from here on. Prompts name bodies and objects, never emotions or "cinematic". When a152take fails, name the failure code and climb the cost ladder (prompt edit → parameter → regenerate → keyframe →153re-plan → fix in the edit). The vendored source skills are in `vendor/` (see `vendor/VENDOR.md`).154155#### The director's sheet (Gates 2–3 as one reviewable page)156157Once the beat sheet exists, turn it into a **plan file** and a review page, and stop until the reviewer has decided158every scene. Non-directors approve better from a sheet than from a chat thread.159160```bash161# plan.json: title, spine, acts, cast (identity strings, sheet images), scenes (purpose, what-the-audience-learns,162# value shift, coverage table, dialogue native+en, continuity, previs image key) — schema in scripts/director_sheet.py163python3 scripts/director_sheet.py build --plan v3/sheet/plan.json --out v3/sheet/sheet.html --images v3/previs v2/refs164```165- Previs: one still per scene (GPT Image 2, 1K medium, cast sheets as `--ref`, the first wide shot as the prompt) so the166 reviewer sees *how it will appear*; put them in an images dir as `previs_<sceneId>.jpg`.167- **In Claude Code:** publish `sheet.html` with the Artifact tool and `capabilities: {db: {}}`. Approvals and notes are168 stored per scene in the artifact db (collection `reviews`, doc id = scene id). Read them back with169 `read_db` (db_op `list`, collection `reviews`), save as `v3/sheet/decisions.json`, then170 `python3 scripts/director_sheet.py decisions --plan … --decisions …` prints the retake/rewrite list and says whether171 Gate 3 is open.172- **Outside Claude Code** (a client, a teammate with the HTML file): the same page works from a file or any host; the173 reviewer presses **Copy decisions as JSON** and sends it back; save that text as `decisions.json`. No server needed.174- Rule: no video generation until `decisions` reports every scene approved. Scenes marked *needs changes* go back to175 the beat sheet, not to the prompt.176177### Step 1: Script & Scene Breakdown178179Write the narration/dialogue script (≈2.5 words/s for VO). Break into scenes: `| Scene | Time | Duration | Setting |180Action | Camera | Mood | VO |`. Durations come from the brief's PACING_TARGET (ads: 2–3.5 s per shot, hook by 2 s;181films: 4–8 s). For **ads/promos** add: product first-reveal time, hero moment, end card + CTA copy, logo placement.182183Define **lock blocks** — identical text pasted into every relevant prompt:184185**Character Lock Block** (one per recurring character):186```187TAKESHI (old) -- Elderly Japanese man, late 70s, thin white hair swept back,188wire-rimmed round spectacles, gentle weathered face. Worn dark brown wool vest189over cream linen shirt, leather work apron with tool pockets.190```191192**Set Lock Block** (one per recurring location, with LEFT/RIGHT positions, surfaces, walls, light sources):193```194WORKSHOP -- Traditional Japanese clock repair workshop. Wooden workbench against195back wall, single oil lamp on the LEFT side of bench. Walls floor-to-ceiling with196clocks: 3 grandfather clocks on the LEFT wall, rows of pendulum clocks above the bench,197pocket watches on hooks to the RIGHT. Window on the LEFT wall. Scattered brass gears,198tweezers, magnifying loupe, small brass bird near the RIGHT edge of the bench.199```200201**Product Lock Block** (ads): exact object, materials, finish, label text, what must NOT change, and the framing rule202from the brief (e.g. "product at 10–15 % of frame, never macro on the pump/logo").203204### Step 2: Reference Images205206```bash207# Character sheet (1:1, clean background)208python scripts/generate_image_gpt.py --prompt "Character reference sheet. Front-facing portrait of: [CHARACTER LOCK]. Neutral background." --output refs/character.png --aspect 1:1 --quality high209# Set wide + set detail210python scripts/generate_image_gpt.py --prompt "[STYLE_LOCK] [SET LOCK]. Wide establishing shot of the full room. Set reference — every object position must be remembered." --output refs/set_wide.png211python scripts/generate_image_gpt.py --prompt "[STYLE_LOCK] Close-up of the workbench surface from [SET LOCK]. Prop reference." --output refs/set_detail.png212# Product: prefer the brand-approved still; if regenerating, --edit product.png (+ --mask) preserves geometry; GPT Image 2 renders label text reliably (quote it)213```214215**Review and approve ALL references before generating any scene stills.**216217### Step 3: Storyboard Stills218219One still per scene, passing every relevant ref (character + set + product, up to 4):220```bash221python scripts/generate_image_gpt.py \222 --prompt "Image 1 is the character (keep the face identical), image 2 is the set (keep the layout). [STYLE_LOCK] [CHARACTER LOCK] [SET LOCK] [SCENE: shot size, camera, action, light]" \223 --ref refs/character.png --ref refs/set_wide.png \224 --output storyboard/scene_02.png --aspect 9:16 --quality medium225```226Generate 3–4 in parallel (background processes). Build a contact sheet and **show the user**:227```bash228ffmpeg -y -i storyboard/scene_%02d.png -filter_complex "scale=480:-1,tile=4x3:margin=4:padding=4" storyboard/contact_sheet.png229```230Consistency checklist: same face / same room layout / same props / same light direction / same palette across scenes.231**Do not generate video until ALL stills are approved.** (Stills cost cents; clips cost dollars.)232233### Step 4: Video Clips234235Pick the model per shot (the brief already suggests one):236237| Feature | Gemini Omni 1.1 Flash | FLUX 3 Video | Seedance 2.5 | Seedance 2.0 | LTX 2.5 | LTX 2.3 |238|---------|------------------------|--------------|--------------|--------------|---------|---------|239| Provider | Google (`generate_video_omni.py`) | Black Forest Labs (`generate_video_flux.py`) | BytePlus Ark (`--model 2.5`) | BytePlus Ark (`--model full/fast/mini`) | LTX (`--ltx-model ltx-2-5-*`) | LTX (`--ltx-model ltx-2-3-*`) |240| Photoreal human faces | ✅ | ✅ | ❌ blocked (Seedream 5 workaround) | ❌ blocked | ✅ | ✅ |241| Duration | 3–10 s (+extend to 40 s) | **5–20 s** (+5–15 s per continuation) | **4–30 s** | 4–15 s | 6–20 s (fast) / 6–10 s (pro) | 2–20 s |242| Max resolution | 4K (upscaled from 720p) | 1080p (`fhd` upsampler) · `--upscale` ×3 | 1080p 10-bit | 4K | 4K (fast) | 4K |243| First + last frame | ✅ | ✅ + up to 10 timed keyframes | ✅ | ✅ | ✅ | ✅ |244| Reference videos (motion/camera) | ✅ 3 × ≤3 s | ❌ (continuation only) | **✅ up to 10** | ✅ 3 | ❌ | ❌ |245| Extend / edit | ✅ extend + conversational edit + `--previous` | continue (v2v) · draft → enhance (same seed) · no edit | edit + extend | edit + extend | ❌ | retake/extend (pro) |246| Multi-shot in one generation | one continuous shot | **✅ `SHOT N: … HARD CUT.`** | time-coded beats | time-coded beats | 2–3, less reliable | 2–3, less reliable |247| Audio | built-in | built-in, **dialogue + lip-sync 13+ languages** | `generate_audio` | `generate_audio` | built-in | built-in |248| Cost / 8 s (approx.) | $0.24 (360p) · $0.80 (720p) · $1.20 (1080p) | $0.48 (draft) · $1.36 (hd) · $2.32 (fhd) | ~$1.2 | ~$1.0 | ~$0.7–1.2 | ~$0.25 |249| Best for | hero shots with people, 360p drafts → 4K, fixing a take by talking to it | dialogue scenes, multi-shot sequences, era/doc formats, storyboard keyframes | 30 s one-shot ads, "camera like @Video1", 3D chars, products | products, motion refs | newest look, long takes | cheap iteration, faces, retakes |250251Veo 3.1 is still wired up as a fallback (`generate_video_veo.py`, see [reference/veo.md](reference/veo.md)). Routing rule of thumb: a shot252with **talking** or **more than one angle** → FLUX 3; a hero shot you will iterate by feel → Omni; a 30 s product one-take with a motion253reference → Seedance 2.5; coverage at scale or a fix inside a clip → LTX. Details: [reference/flux3.md](reference/flux3.md).254255```bash256# Most scenes: image-to-video, prompt = ONLY [CAMERA] + [ACTION] + [LIGHT CHANGE] + [AUDIO]; the still carries the look257python scripts/generate_video_ltx.py --prompt "Camera holds. The rim light breathes brighter over six seconds. Quiet room tone." --image storyboard/scene_01.png --output clips/scene_01.mp4258python scripts/generate_video_omni.py --prompt "In a single continuous shot, slow dolly forward. Ambient: crickets, water. No music." --image storyboard/scene_02.png --duration 6 --output clips/scene_02.mp4259# FLUX 3: dialogue scene from a still — draft at ⅓ cost, then enhance the keeper (same generation, no re-roll)260python scripts/generate_video_flux.py --prompt "Use this image as the first frame. Camera locked at eye level, 70 mm, shallow focus. After a beat she looks up and says, quiet and warm, 'Try it now.' Ambience: rain on the window, a clock tick. No music, no on-screen text, no subtitles." --image storyboard/scene_04.png --duration 8 --draft --output drafts/scene_04.mp4261python scripts/generate_video_flux.py --enhance drafts/scene_04.mp4 --resolution fhd --output clips/scene_04.mp4262# Omni draft loop: 360p first, refine by talking to it, then upscale the keeper263python scripts/generate_video_omni.py --prompt "..." --image storyboard/scene_03.png --resolution 360p --output drafts/scene_03.mp4264python scripts/generate_video_omni.py --previous <id from the run above> --prompt "Slower camera, keep everything else the same" --resolution 360p --output drafts/scene_03_v2.mp4265python scripts/generate_video_omni.py --upscale drafts/scene_03_v2.mp4 --resolution 1080p --output clips/scene_03.mp4266# Seedance 2.5: whole 30 s ad in one shot with the inspiration's camera move as a reference video267python scripts/generate_video_seedance.py --model 2.5 --duration 30 --ratio 9:16 --task-type reference \268 --image refs/product.png --ref-video refs/analysis/_cuts/lateral_track.mp4 \269 --prompt "[STYLE_LOCK] Product like @Image1, camera move like @Video1. 0-3s ... 3-8s ... " --output clips/ad_oneshot.mp4270```271Then **convert every clip to CFR** and trim to the best 2–3.5 s for ads (AI clips are strongest in their first seconds):272```bash273mkdir -p clips/cfr && for f in clips/*.mp4; do ffmpeg -y -i "$f" -r 24 -vsync cfr -c:v libx264 -preset fast -crf 18 -c:a aac "clips/cfr/$(basename "$f")"; done274```275Review each clip (thumbnail strip): face/product match, camera executed, no jitter, physics OK. Regenerate failures.276277### Step 5: Audio278279```bash280python scripts/generate_tts.py --file script.txt --output audio/vo.mp3 --speed 0.95 --timestamps audio/vo_subs.json281python scripts/generate_music.py --prompt "[AUDIO_DIRECTION from brief — style only, no artist names]" --duration 40 --output audio/bgmusic.mp3282python scripts/generate_sfx.py --text "glass pipette click, viscous drop" --duration 3 --output audio/sfx/drop.mp3283# Subtitles from the FINAL vo file (never reuse old timestamps):284whisper audio/vo.mp3 --model base --output_format json --word_timestamps True --output_dir audio/285```286Make the music **at least as long as the film** (the builder warns if not).287288### Step 6: Assemble with HyperFrames289290```bash291scripts/new_hyperframes_project.sh /tmp/my-film/film --aspect 9:16292cp clips/cfr/*.mp4 /tmp/my-film/film/assets/clips/ && cp audio/*.mp3 /tmp/my-film/film/assets/audio/ && cp refs/logo.png /tmp/my-film/film/assets/images/293294python scripts/build_hyperframes_timeline.py --project /tmp/my-film/film \295 --title "NORRA|1.5:sub=Skin, simplified" \296 --clip "assets/clips/scene_01.mp4:3:text=Made for mornings" \297 --clip assets/clips/scene_02.mp4:2.5:trim=1.0 \298 --clip assets/clips/scene_03.mp4:3:audio=1 \299 --end-card "norra.se|3:cta=Shop the serum" \300 --music assets/audio/bgmusic.mp3 --music-volume 0.15 \301 --voiceover assets/audio/vo.mp3 --vo-offset 0.8 --subtitles audio/vo.json \302 --logo assets/images/logo.png --transition crossfade --transition-duration 0.4 --aspect 9:16 --fps 24303304(cd /tmp/my-film/film && npx --yes hyperframes@0.8.27 check) # lint + runtime + layout + motion + contrast305scripts/snapshot_hyperframes.sh /tmp/my-film/film 0.5,3,6,9 # look at PNGs before paying for a render306scripts/render_hyperframes.sh /tmp/my-film/film renders/draft.mp4 --quality draft307scripts/render_hyperframes.sh /tmp/my-film/film renders/film.mp4 # final308```309310`scenes.json` next to `index.html` is the editable timeline (reorder, retime, add `"text"`, swap `"transition"`), then311`--spec scenes.json`. For anything the generator doesn't do (kinetic type, lower-thirds, shader transitions,312audio ducking, sub-compositions), hand-edit `index.html` following [reference/hyperframes.md](reference/hyperframes.md)313or invoke the official `/hyperframes` skills (installed globally). Quick, graphics-free cuts: `scripts/assemble.py` (ffmpeg).314315### Step 7: Review & Iterate316317Watch the render. Fix by layer: pacing → retime `scenes.json`; missing beat → generate/extend a clip (LTX extend,318Seedance 2.5 extend, FLUX 3 `--video` continuation); character/set drift → regenerate the still with the refs; audio → volumes in `scenes.json`; text →319edit `index.html`. Re-run `check`, snapshot the changed timestamps, re-render draft, then final.320321## Product sections — build them natively, never as screenshots322323Screenshots of an app with highlight rings read as slop and two rounds of them were rejected. What works:324325- A **title scene with empty text** that your overlay script fills with real HTML: an agent log that checks off, a real326 post card with a generated photo, channel chips lighting up, a chart drawing with counters, a proposal being approved327 (blueprints: `agent-progress-theater`, `grid-card-assemble`, `constellation-hub`, `dataviz-countup` in328 `/hyperframes-animation`). One oversized world inside a camera wrapper (`transform-origin: 0 0`; to centre world329 point (wx, wy) at scale s: `x = FW/2 − wx·s, y = FH/2 − wy·s`) and pan between stations on the spoken words.330- Set `text-align: left; line-height: 1.25` on the injected root (title cards centre everything) and use the real logo331 mark in cards, never a coloured square.332- If a real screenshot plate must appear, **measure** its boxes (`measure_ui.py`) and map css → frame333 (`x·4/3, y·4/3 − 60` for a 1440×900 plate covered into 16:9); guessed ring coordinates were wrong three times out of three.334- Live-action cutaways after the interface break the "motion-first" flow for evaluators; if one is needed, bake the real335 product chart onto its screen (`generate_image_gpt.py --edit still.png --ref chart.png`) before animating it.336337## Word-synced editing338339Every era push, kinetic card and camera move lands on a spoken word, not on a guess:3403411. `generate_tts.py --timestamps audio/vo_words.json` (character timestamps).3422. Schedule VO lines sequentially with a fixed gap; size each scene so the next anchored scene starts at343 `line.start − offset` (+ the transition overlap), clamped to the source length. Era offsets of −0.05 s make the push344 land *on* the line, not before it.3453. `vo_words.py --anchor "<line start>" "<phrase>"` gives the offset of a phrase inside a line → key tweens to346 `line.start + offset`. Long slots: join two takes with a hard cut so each phrase has its picture347 (`master.sh join`); slow a short take with `setpts` (1.18× is invisible on hand-cranked footage).3484. Music "hard stop" requests are ignored by the generator — build the stop in the mix (trim + silence + sub pulse +349 looped calm section).350351## Archival / era footage that reads as real352353`GPT Image 2 still → Omni image-to-video`, not text-to-video:354355- Still prompt as an *authentic archival photograph*: name the process (glass plate, silver gelatin, Kodachrome, 35 mm356 consumer film + flash, camcorder), period clothing, **real faces** (Omni accepts them; Seedance rejects them in inputs),357 plate damage/grain/vignette, "no readable text, no modern objects".358- Omni prompt = era artefacts + **natural, moderate, continuous motion in one locked-off setup**: "no one enters or359 leaves, no new figures, hands keep their shape, no pans, no cuts". "Hold still" direction scores *lower* (frozen).360 Known failures: phantom figures after ~4 s (use the first 3 s), a whip-pan cut when two actions are listed, floating361 sci-fi UI when notifications are mentioned near a face, garbled text on generated screens.362- Two takes per slot → `pick_takes.py` (authenticity / motion / era / usability + artefact timestamps + best in-point).363- 720p drafts; finals via `--upscale` (the script refines the original interaction, which works from the EEA and is a364 true upscale). Conform with `master.sh conform` (crf 14, lanczos, 24 fps CFR).365366## Quality gate — how to use the jury honestly367368- Run `jury.py` **blind**, twice, with the explicit scale. Feeding it previous scores anchors it (it returned 7/10 for six369 rounds while calling every fix "improved"); use `--anchored` only to ask what moved.370- Blind Gemini tops out around 6–7 for generated plates; when its notes turn *structural* (a live-action cutaway371 after the UI, the tone at the turn) rather than about detail, more generations will not move it — say so and ship.372- Act on notes in this order: sync (pushes on lines) → product clarity (native beats) → B2B proof (pipeline, CPL,373 hours saved, an approve click) → end card (mark assembles, high-contrast CTA, risk-reversal line) → footage.374375## Finals, masters and aspect variants376377- `master.sh render <project> renders/final.mp4`: high-quality render → `loudnorm I=-14 TP=-1.5 LRA=11` → AAC 256k →378 `+faststart`; keep the `_raw` next to it. `master.sh probe` reports dims, fps, LUFS, true peak.379- Write overlays with `FW/FH` and helper `cx/cy` from day one; a `ASPECT=9:16 OUT_DIR=../film_916` switch (module380 60..1020 × 520..1060, axis at 1180, pushes ±FW, UI plates as cards with their own mapping, live action inside a381 module) turns the same build into the vertical cut. After any templating pass run382 `grep -o 'top:{F[HW][^"]*' index.html` — an unevaluated brace silently drops the element onto the class position.383- Cost: `cost_report.py <project>` before the client asks. A 50 s film with two footage rounds ran ≈ $65 in API spend384 (Seedance ≈ $30, Omni ≈ $22, stills ≈ $8, audio ≈ $4).385- Ask for the **tagline and real metrics at intake**; placeholders survive to the master otherwise.386387---388389## Film Types — what changes390391| | Short film | Ad / commercial | Product promo / demo | Social reel |392|---|---|---|---|---|393| Length | 60–180 s | 6 / 15 / 30 s | 20–60 s | 7–30 s |394| Aspect | 16:9 | 16:9 + 9:16 cutdown | 16:9 or 9:16 | 9:16 |395| Shot length | 4–8 s | 2–3.5 s, hook ≤ 2 s | 3–5 s | 1.5–3 s |396| Step 0 refs | 1 film in the genre | 2–3 ads in the category | competitor demo + one brand film | 3 top reels |397| Must-have beats | arc: setup → tension → climax → resolution | hook, product reveal, hero moment, **end card + CTA + logo** | problem → product → proof → CTA | hook in first frame, caption-first, loopable end |398| Faces | Omni / FLUX 3 / LTX | often none (product) → Seedance 2.5 shines | hands only → Seedance / LTX | UGC feel → LTX + documentary prompt stack |399| Typography | title + credits | headline overlays, price/claim, CTA pill | feature callouts | burned-in captions (`--subtitles`) |400| Audio | VO + score + SFX | music-led, 1–2 VO lines, SFX hits on cuts | VO-led, light bed | music + captions, VO optional |401| Assembly | `--transition blur/dip`, fades | `--transition cut/crossfade 0.3`, `--end-card ...:cta=` | `--clip ...:text=` callouts | `--fps 30`, `--aspect 9:16`, `--fade-out 0` for loops |402403Ads: also render the 16:9 master **and** a 9:16 cutdown (`--aspect 9:16` with re-framed stills or `--resolution`),404and a 6 s bumper from the hero shot + end card.405406---407408## Visual Consistency Rules409410Three layers, each enforced by a reference image **and** a lock block in every prompt:411412| Layer | What | How |413|-------|------|-----|414| Character | face, hair, clothes, build | character sheet `--ref` + Character Lock Block |415| Set / location | layout, walls, furniture, light direction, palette | set wide/detail `--ref` + Set Lock Block |416| Props / product | objects don't teleport; product geometry/label intact | detail `--ref` + Product Lock Block; no macro on hardware/logos; typography in post |417418Reference images beat prompt tokens: with strong refs the video prompt is 30–80 words of pure camera/action/audio. See419[reference/learnings.md](reference/learnings.md) for the failure modes (LTX particle triggers, Seedance photoreal420threshold, Nano Banana geometry drift) and fixes.421422## Prompting Cheat Sheet423424- **Images (GPT Image 2):** `[Subject + adjectives] + [Action] + [Location] + [Composition/Camera] + [Light] + [STYLE_LOCK]` — full sentences, brief an art director. Quote any on-image text (*the label reads "NORRA"*). Name what each `--ref` is for. See [reference/gpt-image.md](reference/gpt-image.md).425- **Gemini Omni:** *"In a single continuous shot, [camera rig + move]. [What changes, with timing: 'after 3 seconds ...']. [Light]. Audio: [what you want] — no music / no dialogue if unwanted."* Refs as `<IMAGE_REF_n>`. Edits: short + *"Keep everything else the same."* See [reference/omni.md](reference/omni.md).426- **In-model effects vs. post:** ask a video model for *diegetic* effects only (smoke, light, particles, physical transformations). Text, logos, numbers and UI always go through HyperFrames (see Intake).427- **LTX 2.3 / 2.5:** one flowing present-tense paragraph; i2v = describe the *change*, not the subject; never mention particles/dust/fluids unless wanted; "Camera holds." for static.428- **FLUX 3:** brief a colleague in prose — it rewrites the prompt and fills every layer you leave unnamed. Name camera + subject/action + light + motion quality + **each audio layer** (speech in quotes with a *visible* speaker, ambience, effects, music or "no music") and end with *"no on-screen text, no subtitles"*. i2v opens *"Use this image as the first frame."* + what to leave alone. Multi-shot: `SHOT ONE: … HARD CUT. SHOT TWO: …`, ~5 s per shot, adjacent shots must contrast. Continuation opens *"Continue the reference video from its final frames."* Draft first. See [reference/flux3.md](reference/flux3.md).429- **Seedance 2.5 / 2.0:** `[Style preamble] + [Lock blocks] + [Scene] + time-coded beats (0-3s / 3-6s ...) + [Light/mood] + [Transition hint]`; name every reference's role (`@Image1` = product, `@Video1` = camera move). For 30 s one-shots write 6–8 time-coded beats incl. the end card. See `/seedance-prompt`.430- **Lock blocks** go into EVERY prompt for that character/set/product, verbatim.431- **One job per reference** (Seedance): "@Image 1 defines the product geometry only. Ignore the background." Conflicts resolve identity → prop → wardrobe → location → light → camera.432- **Say the sound** (LTX/Omni/Veo): anything you leave out gets invented; attach every sound to something in frame; "no music" when you score in post.433- Worked, sourced examples for each of these: [reference/examples.md](reference/examples.md).434435## Error Recovery436437| Problem | Fix |438|---------|-----|439| Model not found | `python scripts/list_models.py` — names change; LTX `ltx-2-*` ids retired Aug 2026; Omni = `gemini-omni-1.1-flash`; GPT Image = `gpt-image-2` |440| Omni: `no video in response` / HTTP 4xx | check the prompt for policy triggers; editing/extending uploaded video is unavailable in EEA/CH/UK; recover a finished output with `--download <interaction_id>` |441| Omni `--upscale` → "Exactly one input video is required for edit task" | uploaded-video edits are blocked in EEA/CH/UK; the script now refines the original interaction from `<clip>.mp4.json` (`--previous`) — keep sidecars next to clips |442| HyperFrames `gsap_exit_missing_hard_kill` | a fade ends within ~0.1 s of the next clip's `data-start` — move it earlier or add a `tl.set` |443| Overlay element sits in the wrong place after a templating pass | an unevaluated `{FH …}` in a plain string → invalid CSS → class position wins; `grep -o 'top:{F[HW]' index.html` |444| Highlight rings / cursor off the UI element | never guess: `measure_ui.py` and map css → frame |445| Omni clip too short / long | there is no duration field — `--duration N` writes it into the prompt; chain `--extend` for > 10 s |446| GPT Image 2 rate limit (5 img/min tier 1) | generate ≤4 stills in parallel; use `--quality medium` for coverage |447| Seedance `InputImageSensitiveContentDetected` | photoreal face → use Omni/LTX, or Seedream 5 text-only still → Seedance ([reference/seedance.md](reference/seedance.md)) |448| `media_host: no hosting configured` / S3 upload failed | set `S3_BUCKET` + `S3_REGION` in `~/config.env`, run `python scripts/media_host.py check`; without `S3_PUBLIC_URL` the script hands Ark a 24 h presigned URL (bucket can stay private) |449| Seedance `InvalidParameter.TaskTypeConstraint/Mismatch` | edit/extend/first-frame need `ratio=adaptive` (script forces), edit needs `duration=-1` and edit/remove/replace words in the prompt |450| Character / set drift | same `--ref` images every time + lock blocks; extract a clean frame from the first good clip as the new character ref |451| Props appear/disappear | put all props in the Set Lock Block from scene 1 |452| Video jitter / freeze at end of a scene | CFR convert; slot longer than source → shorten `--clip` seconds or LTX `extend` |453| `hyperframes lint` errors | read the message — usually a `<video>` inside a timed wrapper, a missing audio `id`, or `crossorigin`; see [reference/hyperframes.md](reference/hyperframes.md) |454| `check` contrast failure on overlay text | keep the text-shadow, add a scrim, or move the overlay off bright footage |455| Music shorter than film | regenerate with `--duration ≥ film length`; the builder clamps and warns |456| Subtitles out of sync | re-run whisper on the **final** vo file; check `--vo-offset` matches |457| ElevenLabs music TOS error | remove artist/brand names |458| Gemini video analysis fails on URL | non-YouTube URLs are downloaded with y459460…(truncated)