render-myth-vs-fact
The free, deterministic renderer for the myth-vs-fact video ad format — the calm,
sound-off-safe kinetic-typography explainer that busts N common myths and hands the viewer
a credible resolution. Red-strike MYTH cards flip to teal-check FACT cards over a
calm-authority VO, then a "what actually works" turn + an optional proof reveal + a
punch line + a static brand end card.
Every on-screen word is a deterministic HTML hyperframe — no AI image/video gen, no
b-roll, no character. Visuals cost $0. The only metered spend is upstream (VO +
Whisper word-timestamps + an optional music bed), gated to its own capabilities. This
capability OWNS the whole FREE assembly: beat-snap → render → captions → mix → burn →
master. Iterate the cut for free; re-roll only the offending paid audio beat.
It ports the validated build from the Clinikally "acne myths" run — brand-neutralised,
config-driven, and portable (no /Users, no clients/; everything via --config +
--work-dir).
SOUND-OFF SAFE is the whole point: every claim is legible on-screen and the VO only
reinforces it. VO-FIRST: render the VO, extract Whisper word onsets on the RENDERED
audio, then snap every beat boundary + strike wipe + reveal to those onsets.
The 8-beat spine (roles)
hook → 3× myth-fact (the flip triad — identical grammar so it reads as a pattern) →
turn (the "what actually works" pivot) → proof (optional actives/proof reveal, omit if
empty) → punch (full-frame closer) → end-card (the static brand PNG). Each beat carries
its role, duration, and its copy; ONE role template renders any pair.
Scripts (free — Python + Playwright + ffmpeg, no paid calls)
scripts/beat_snap.py — VO-first alignment. FAL Whisper word-timestamps on the RENDERED
VO → re-snap every beat boundary to the nearest word onset. Writes
beat-manifest.json + whisper/words-flat.json into the work dir. --no-whisper keeps
the config durations un-snapped for a fully offline run.
scripts/render_beats.py — the deterministic renderer. Per mg beat: pick the role
template under hyperframes/, inject the beat's copy + palette + fonts as window.BEAT,
drive window.renderAt(t) frame-by-frame via Playwright, screenshot each frame → ffmpeg
at EXACTLY the configured fps (default 25/1). The end-card beat is built from the
pre-supplied brand PNG (scale/crop + a ~0.35s fade-up) — never generated per run.
scripts/make_captions.py — karaoke .ass from the manifest + Whisper words. ≤3 words
per cue; close on a >0.4s gap / beat-window edge / sentence-ending punctuation. Captions
are burned ONLY in caption-allowed windows; the proof + end-card beats are suppressed.
scripts/compose.py — the assembler: concat the beats → mix VO + optional music (music
−20 dB, amix normalize=0, ~0.8s tail fade) → burn the .ass LAST → master mp4.
scripts/config.example.json — the shape of the brand config the recipe binds (the
brand-neutralised Clinikally values as a worked reference).
scripts/hyperframes/ — the bundled hyperframe scaffold: _shared.css (palette-tokened
tokens + card/tag/fact/pill/chain type), _shared.js (the initRenderer /
springScale / buildLineStrikes + strikeLines per-line-strike / popIn /
revealWords helpers + config injection), and one template per role (beat-hook.html,
beat-myth-fact.html, beat-turn.html, beat-proof.html, beat-punch.html).
Inputs (all via --config + a runtime work dir — NO hardcoded paths)
config.json carries: fps (25) / width / height; vo + optional music +
mix{music_db:-20, tail_fade:0.8}; palette (the five CSS-var tokens bg, myth_strike,
fact_accent, headline_ink, accent); brand_name; display_font; end_card_png +
end_card_fade; caption_style; suppress_beats; and beats[] — each {n, role, duration, captions, cues{...}} plus the role's copy:
- hook:
eyebrow, hook_line, emphasis, strike_word
- myth-fact:
myth_index, myth_line, and either fact_line (a [bracketed] phrase
becomes the accented payload) OR fact_clauses[] (a staggered clause chain)
- turn:
turn_slate, turn_sub ([brackets] → emphasis)
- proof:
proof_eyebrow, proof_items[] ({name, badge}), proof_footnote
- punch:
punch_line
- end-card: none (built from
end_card_png)
The recipe's myth_fact_pairs, hook_line, turn_slate, punch_line, palette,
end_card_png, and optional actives_or_proof map onto these beats 1:1. See
config.example.json.
Craft rules (load-bearing — faithful to the source molecule)
- MYTH strikethrough is PER-LINE. Measure each wrapped line box (
Range.getClientRects,
deduped to one rect per visual line) and lay one red bar at each line's vertical MIDDLE,
driven as ONE continuous L→R sweep. A single fixed-Y rule reads as an underline the moment
the headline wraps.
- The end card is the pre-built brand PNG — NEVER generated per run. Scale/crop to the
canvas with a short fade-up; it carries its own baked logo + claim + CTA.
- Animation is a pure function of beat-local time — no
setTimeout, no CSS keyframes —
so Playwright seeks frame-exact and the render is fully reproducible.
- Every beat mp4 is exactly the configured fps (25/1).
render_beats.py enforces +
warns; a mismatch makes the concat demuxer silently drop frames.
- All text fits the 88% safe area at the ~15% spring-overshoot PEAK, not at rest.
- Captions burned LAST, ≤3 words/cue, closing on >0.4s gap / window edge / sentence end.
The ASS
Events Format: line MUST carry the Name field or every cue gets a
leading-comma artifact. Suppress the proof/footnote + end-card beats (two text layers at
one spot both go unreadable).
- Mix constants are validated — music
−20 dB under the VO, amix normalize=0 (with
normalize on the bed pumps), ~0.8s tail fade. Sound-off must still work without the bed.
- Keep the MYTH triad's flip grammar + internal timing identical so it reads as a
pattern (anaphora).
Requires
- Python 3 + Playwright chromium (
pip install playwright && playwright install chromium) for the frame-exact hyperframe render, and ffmpeg/ffprobe on PATH. If
Playwright is unavailable, compose.py + make_captions.py (the concat / mix / caption
path) still run; only render_beats.py needs the browser.
watch (QC the final master — the red strike crosses the vertical MIDDLE of EVERY wrapped
myth line, the VO is intelligible, captions are legible with no card collision,
suppression is correct on the proof + end-card beats, framerate is uniform 25/1, no
clipping, every claim is legible sound-off). The recipe gates the paid create-vo-eleven
(VO), create-music-elevenlabs (bed), and FAL Whisper calls — this capability itself
makes NO paid calls.
1---2name: render-myth-vs-fact3description: Assemble a myth-vs-fact kinetic-typography explainer video ad (≈29.5s, 9:16) from N myth/fact pairs + hook / turn / punch copy + palette + a brand end-card PNG + a VO track — a hook, 3 red-strike MYTH cards that flip to teal-check FACT cards (per-line strikethrough that crosses EVERY wrapped line), a "what actually works" turn, an optional proof reveal, a punch line, and a static end card. DETERMINISTIC assembly with ZERO AI-gen visuals — HTML hyperframes rendered frame-exact via Playwright (`window.renderAt(t)`, animation a pure function of beat-local time), Whisper beat-snap to VO word onsets, concat at a uniform fps, karaoke `.ass` captions burned last (suppressed on the proof + end-card beats), and a VO + optional music mix (music −20 dB, `amix normalize=0`, tail fade). FREE (Python + Playwright + ffmpeg); the recipe supplies the copy / palette / end-card / VO and gates the paid VO / music / Whisper calls to their own capabilities. Use for the myth-vs-fact format.4---5
6# render-myth-vs-fact
7
8The free, deterministic renderer for the **myth-vs-fact** video ad format — the calm,
9sound-off-safe kinetic-typography explainer that busts N common myths and hands the viewer
10a credible resolution. Red-strike **MYTH** cards flip to teal-check **FACT** cards over a
11calm-authority VO, then a "what actually works" **turn** + an optional **proof reveal** + a
12**punch** line + a static brand **end card**.
13
14Every on-screen word is a **deterministic HTML hyperframe** — **no AI image/video gen, no
15b-roll, no character**. Visuals cost **$0**. The only metered spend is upstream (VO +
16Whisper word-timestamps + an optional music bed), gated to its own capabilities. This
17capability OWNS the whole FREE assembly: beat-snap → render → captions → mix → burn →
18master. Iterate the cut for free; re-roll only the offending paid audio beat.
19
20It ports the validated build from the Clinikally "acne myths" run — brand-neutralised,
21config-driven, and portable (no `/Users`, no `clients/`; everything via `--config` +
22`--work-dir`).
23
24**SOUND-OFF SAFE is the whole point:** every claim is legible on-screen and the VO only
25reinforces it. **VO-FIRST:** render the VO, extract Whisper word onsets on the RENDERED
26audio, then snap every beat boundary + strike wipe + reveal to those onsets.
27
28## The 8-beat spine (roles)
29
30`hook` → 3× `myth-fact` (the flip triad — identical grammar so it reads as a pattern) →
31`turn` (the "what actually works" pivot) → `proof` (optional actives/proof reveal, omit if
32empty) → `punch` (full-frame closer) → `end-card` (the static brand PNG). Each beat carries
33its `role`, `duration`, and its copy; ONE role template renders any pair.
34
35## Scripts (free — Python + Playwright + ffmpeg, no paid calls)
36
37- `scripts/beat_snap.py` — VO-first alignment. FAL Whisper word-timestamps on the RENDERED
38 VO → re-snap every beat boundary to the nearest word onset. Writes
39 `beat-manifest.json` + `whisper/words-flat.json` into the work dir. `--no-whisper` keeps
40 the config durations un-snapped for a fully offline run.
41- `scripts/render_beats.py` — the deterministic renderer. Per `mg` beat: pick the role
42 template under `hyperframes/`, inject the beat's copy + palette + fonts as `window.BEAT`,
43 drive `window.renderAt(t)` frame-by-frame via Playwright, screenshot each frame → ffmpeg
44 at EXACTLY the configured fps (default 25/1). The `end-card` beat is built from the
45 pre-supplied brand PNG (scale/crop + a ~0.35s fade-up) — **never generated per run**.
46- `scripts/make_captions.py` — karaoke `.ass` from the manifest + Whisper words. ≤3 words
47 per cue; close on a >0.4s gap / beat-window edge / sentence-ending punctuation. Captions
48 are burned ONLY in caption-allowed windows; the proof + end-card beats are suppressed.
49- `scripts/compose.py` — the assembler: concat the beats → mix VO + optional music (music
50 −20 dB, `amix normalize=0`, ~0.8s tail fade) → burn the `.ass` LAST → master mp4.
51- `scripts/config.example.json` — the shape of the brand `config` the recipe binds (the
52 brand-neutralised Clinikally values as a worked reference).
53- `scripts/hyperframes/` — the bundled hyperframe scaffold: `_shared.css` (palette-tokened
54 tokens + card/tag/fact/pill/chain type), `_shared.js` (the `initRenderer` /
55 `springScale` / `buildLineStrikes` + `strikeLines` per-line-strike / `popIn` /
56 `revealWords` helpers + config injection), and one template per role (`beat-hook.html`,
57 `beat-myth-fact.html`, `beat-turn.html`, `beat-proof.html`, `beat-punch.html`).
58
59## Inputs (all via `--config` + a runtime work dir — NO hardcoded paths)
60
61`config.json` carries: `fps` (25) / `width` / `height`; `vo` + optional `music` +
62`mix{music_db:-20, tail_fade:0.8}`; `palette` (the five CSS-var tokens `bg`, `myth_strike`,
63`fact_accent`, `headline_ink`, `accent`); `brand_name`; `display_font`; `end_card_png` +
64`end_card_fade`; `caption_style`; `suppress_beats`; and `beats[]` — each `{n, role,
65duration, captions, cues{...}}` plus the role's copy:
66- **hook:** `eyebrow`, `hook_line`, `emphasis`, `strike_word`
67- **myth-fact:** `myth_index`, `myth_line`, and either `fact_line` (a `[bracketed]` phrase
68 becomes the accented payload) OR `fact_clauses[]` (a staggered clause chain)
69- **turn:** `turn_slate`, `turn_sub` (`[brackets]` → emphasis)
70- **proof:** `proof_eyebrow`, `proof_items[]` (`{name, badge}`), `proof_footnote`
71- **punch:** `punch_line`
72- **end-card:** none (built from `end_card_png`)
73
74The recipe's `myth_fact_pairs`, `hook_line`, `turn_slate`, `punch_line`, `palette`,
75`end_card_png`, and optional `actives_or_proof` map onto these beats 1:1. See
76`config.example.json`.
77
78## Craft rules (load-bearing — faithful to the source molecule)
79
80- **MYTH strikethrough is PER-LINE.** Measure each wrapped line box (`Range.getClientRects`,
81 deduped to one rect per visual line) and lay one red bar at each line's vertical MIDDLE,
82 driven as ONE continuous L→R sweep. A single fixed-Y rule reads as an underline the moment
83 the headline wraps.
84- **The end card is the pre-built brand PNG — NEVER generated per run.** Scale/crop to the
85 canvas with a short fade-up; it carries its own baked logo + claim + CTA.
86- **Animation is a pure function of beat-local time** — no `setTimeout`, no CSS keyframes —
87 so Playwright seeks frame-exact and the render is fully reproducible.
88- **Every beat mp4 is exactly the configured fps (25/1).** `render_beats.py` enforces +
89 warns; a mismatch makes the concat demuxer silently drop frames.
90- **All text fits the 88% safe area at the ~15% spring-overshoot PEAK**, not at rest.
91- **Captions burned LAST**, ≤3 words/cue, closing on >0.4s gap / window edge / sentence end.
92 The ASS `Events Format:` line MUST carry the `Name` field or every cue gets a
93 leading-comma artifact. Suppress the proof/footnote + end-card beats (two text layers at
94 one spot both go unreadable).
95- **Mix constants are validated** — music `−20 dB` under the VO, `amix normalize=0` (with
96 normalize on the bed pumps), ~0.8s tail fade. Sound-off must still work without the bed.
97- **Keep the MYTH triad's flip grammar + internal timing identical** so it reads as a
98 pattern (anaphora).
99
100## Requires
101
102- **Python 3** + **Playwright chromium** (`pip install playwright && playwright install
103 chromium`) for the frame-exact hyperframe render, and **ffmpeg/ffprobe** on PATH. If
104 Playwright is unavailable, `compose.py` + `make_captions.py` (the concat / mix / caption
105 path) still run; only `render_beats.py` needs the browser.
106- `watch` (QC the final master — the red strike crosses the vertical MIDDLE of EVERY wrapped
107 myth line, the VO is intelligible, captions are legible with no card collision,
108 suppression is correct on the proof + end-card beats, framerate is uniform 25/1, no
109 clipping, every claim is legible sound-off). The recipe gates the paid `create-vo-eleven`
110 (VO), `create-music-elevenlabs` (bed), and FAL Whisper calls — this capability itself
111 makes NO paid calls.