Documentation
README
render-3d-character-explainer
The free, deterministic renderer for the 3d-character-explainer video ad format ā the glossy Pixar-style 3D spot built on an "N types of X" listicle spine, where a recurring human protagonist plus a locked cast of N persona characters (one per list item) carry a hook ā "deeper story" ā cast-reveal ā one beat per list item ā kicker ā product test ā relieved payoff. This capability is the FREE assembly stage only. All generative work (Nano-Banana cast anchors + per-scene keyframes, Kling-V3 i2v clips, ElevenLabs VO + music, or a source ad's audio reused verbatim) happens upstream in the recipe and is handed to this capability as files.
It ports the validated compose recipe from the Bristle "Six Types" restyle run
(_render_full.sh ā per-scene trim ā normalize 1080Ć1920/fps30 ā concat -c copy ā mux the
source audio, with a static-still fallback on any failed clip). The assembly is
deterministic ā iterate the cut for free, re-roll only the offending paid beat.
Two modes
- Restyle mode (
audio_mode: "restyle", the reference run) ā re-tell a finished source ad, beat for beat, as 3D character comedy. The source ad's audio mix (VO + music bed) is reused VERBATIM (source_audio), and the per-scenetarget_sectable is inherited from the source's scene timing. No new VO or music is rendered. The trims must sum to the source audio length. - Original mode (
audio_mode: "original") ā the ad authors its own narration. Each scene carries a measured VO cue (scenes[].vo,target_sec= the ffprobe'd VO duration) which is concatenated into a VO track (loudnorm I=-14) and optionally mixed under amusic_bed(loudnorm I=-26 thenvolume,amix normalize=0).
What it does (the deterministic recipe)
- Per-scene retime. Each i2v clip is trimmed to its scene
target_secand normalized to identical dims/fps/SAR (scale=W:H:force_original_aspect_ratio=decrease,pad=W:H:(ow-iw)/2:(oh-ih)/2:color=<pad>,fps=30,setsar=1). A clip shorter than its window is extended withtpad=stop_mode=clone; a longer one is-ttrimmed. Decrease+pad (never crop) preserves the full 9:16 keyframe framing. - Static-still fallback. For any scene whose
clipis missing or failed to render, the scene'skeyframePNG is looped (-loop 1) fortarget_sec, so the master always assembles. Fallback scenes are printed at the end. - Identical re-encode + concat. Every segment is re-encoded
libx264 -crf 18 -pix_fmt yuv420p -r 30even if already correct ā a dims/framerate mismatch makes the concat demuxer silently drop frames ā then concatenated via the concat demuxer (-c copy). - Audio. Restyle:
source_audiomuxed verbatim (-map 0:v -map 1:a), clamped to the video length. Original: per-scene VO track (optionalatempo,apad,-tclamp) ā loudnorm ā optionally mixed under the music bed. - Captions last.
make_captions.pyemits a libass.ass(one cue per scene,start = scene_start + 0.08s, suppressed on any scene with no caption ā e.g. a product/end-card beat carrying its own typeset copy).compose.pyburns it as the final filter so captions sit on top. Word-level energy-pop captions (Whisper on the narration) are the recipe's upstream option ā produce that.assexternally and pointcaptions_assat it; compose burns whatever.assit's handed.
Scripts (free ā Python + ffmpeg, no bash, no paid calls)
scripts/make_captions.pyā emits the per-scene libass.assfrom the SAME scene table compose reads, so caption windows stay in lockstep with the cut. Run beforecompose.py(or leavecaptions_assunset / pointing at nothing to skip captions).scripts/compose.pyā the assembler: per-scene trim + identical 1080Ć1920/30fps re-encode (static-still fallback on missing clips) ā concat ā audio (restyle verbatim / original mix) ā burn captions ā master mp4.scripts/config.example.jsonā the shape of theconfigthe recipe binds (the brand-neutralised "Six Types" restyle values as a worked reference).
Inputs (all via --config + a runtime work dir ā NO hardcoded paths)
config.json carries: audio_mode (restyle | original), scenes[] (each {id, clip, keyframe, target_sec, caption?, vo?, atempo?} where target_sec is the source-inherited
window in restyle mode or the measured VO window in original mode, and keyframe is the
static-still fallback source), source_audio (restyle), music_bed + music_volume +
atempo (original), width/height (default 1080Ć1920), pad_color (letterbox colour),
captions_ass, and caption_style. See config.example.json.
Craft rules (load-bearing ā faithful to the source molecule + reference run)
- Restyle inherits the source timing. A restyle reuses the source ad's exact audio, scene order, and per-beat durations verbatim; only an original-mode remix authors its own VO + timing table. Merge any sub-1.5s flash scene into a neighbour upstream to avoid a dead micro-cut (the reference folded scene 7 into scene 8).
- Normalize decrease+pad, never crop ā the listicle's cast-reveal + per-persona framing must not lose edges; letterbox-pad to the canvas colour instead. Re-encode every segment to 30fps before concat, even if already correct, or the concat demuxer silently drops frames.
- Static-still fallback is mandatory ā Kling can 403 mid-run (a billing wall after a burst of successes, not a rate limit). Any failed clip loops its keyframe so the master still assembles; re-roll only the missing beat and recompose (free).
generate_audiowas false upstream ā Kling would otherwise invent its own dialog track; the real narration is muxed here separately. (This is the recipe's upstream call, not this capability.)- No AI-rendered brand text ā the product-beat keyframe shows a BLANK-label box; the real wordmark/end-card copy is composited upstream, never AI-drawn. Suppress captions on any product/end-card beat (its typeset copy carries the message ā two text layers at one spot are both unreadable).
- Caption
start = scene_start + 0.08s(avoids the caption flashing a frame before a cut).
Requires
watch (QC the final master ā the human protagonist reads as the SAME person every scene
(wardrobe/hair/lighting held), each persona is on-model, the cast-reveal lineup matches the N
list items, the product beat shows the REAL box, narration lands beat-for-beat, and duration is
within ±0.1s of the summed windows). The recipe gates the paid create-image-fal (cast
anchors + keyframes), create-video-fal (Kling-V3 i2v), create-vo-elevenlabs, and
create-music-elevenlabs calls to their own capabilities ā this capability itself makes NO
paid calls.