Reaction duet — beat sheet, line rules, top-track cut list, composite
Runtime 20-60 s, target 35-45 s (≈120-150 words at ~3.4 wps). Percentages are of runtime; beats are pack-line ranges, never mid-sentence. The top panel switches only on line boundaries (times from lines.json).
| # | Beat | % | Lines | Speaker | Top panel | Bottom panel (intent) | Line rules |
|---|---|---|---|---|---|---|---|
| 1 | Source hook | 0-15 | 1-3 | MOM | Q card 1 appears at 0.0 s | Mom at the kitchen table, phone in hand, stops scrolling; then reads (eyes down, profile) | Line 1 is the card text verbatim or "A mom posted this." + the card read in line 2. Symptom/stake named by line 2. No product. |
| 2 | Recognition | 15-35 | 4-10 | MOM | Q card 1 held; one phrase highlighted (card 1b) | Literal inserts of HER morning (third knock at the door, alarms going off on the nightstand, her watching the clock) | "That's my house." class line, then her ritual: counts/times from VOC are allowed in video. Failed solutions generic (louder alarms, phone across the room, lights on): never brand names, never bed shakers. |
| 3 | Stake | 35-50 | 11-14 | MOM | Q card 2: the escalation line (same post, or a second sourced post on the same stake) | Close on her face LISTENING while she reads card 2 aloud off-screen; then the letter on the table (a shape with a red stamp) | The cost lands on the mom (letter in her name, CPS warning, the fight). Mornings caused it. Stake-bank row cited. |
| 4 | Vindication turn | 50-65 | 15-18 | MOM (+CARD ≤2 lines) | E card (sleeping brain under sound waves + one plain sentence) or Q card held | Mom side-on, leaning toward the phone, OTS on the screen | "You're not failing." to the poster. Fish's ruled root-cause line verbatim (sound doesn't reach a deep-sleeping brain). Science is the authority, not a person. |
| 5 | Answer | 65-85 | 19-23 | MOM | R: real band footage, crop-filled | Kid asleep, wrist resting on the duvet, band side-on, display dark; eyes open with small motion lines | Mechanism lines: touch gets through; silent to everyone else; keeps going until he turns it off. Product named ONCE. |
| 6 | Payoff + CTA | 85-100 | 24-26 | MOM | End card (optional, --end-card; off by default per Fish 10-06) spans both halves (last 2.5 s) |
Calm kitchen; kid already at the table | Independence/calm on HER ("He's downstairs before me now." class). Last line: product name + 60-night trial. |
Line rules (all beats)
- One sentence per pack line, 5-12 words. MOM's own lines pass the said-out-loud test and are pronoun-first.
- Quote lines are verbatim. Trim from the front/back only, never splice two sentences, never fix grammar beyond dropping a typo the voice would stumble on. ≤ ~15 words per card (dawn-statics rule: pick a shorter real line before cramming).
- The quote is read by MOM (or shown silent while she reacts), never by a "news" voice.
- Numbers spelled with spaces for EL ("six thirty"); captions keep the script spelling.
- She talks TO the poster and the viewer ("you"), not about the product. No "I bought…", no "I've been using…", no star ratings.
- Product ≤25 % of runtime.
Top-track cut list (the ## Top track section of the pack)
| card | kind | first line | last line | source id | text / file |
|---|---|---|---|---|---|
| 1 | Q | 1 | 10 | reddit <post id> (thread file) |
verbatim line |
| 2 | Q | 11 | 14 | reddit <post id> |
verbatim line |
| 3 | E | 15 | 18 | ours | cards/e1.png + one sentence |
| 4 | R | 19 | 23 | own b-roll | clip path + in-point |
| — | end card (optional, --end-card; off by default per Fish 10-06) |
last 2.5 s | — | endcard.py | end_card text |
Each card holds from its first line's start (card 1 from 0.0) to the next card's first line start; the last card runs to the end of the reactor cut.
Composite (preferred since 10-06: Remotion DuetStack, SKILL §9; the manual ffmpeg below is the fallback; split.py NOT BUILT)
Cartoon reactor = 720×1280. Captions burn at ~57-62 % of the reactor frame (Remotion tiktok mode, moved up 10-06); crop=720:640:0:560 keeps rows 560-1200, so captions land at ~63-68 % of the composite, just below the seam; the bottom of that is inside Meta Reels' bottom 35 % UI band (safe box = y 14-65 %), so preview it, or use DuetStack (captions at ≤64 %). Photoreal reactor (ugc-omni, 1080×1920, captions at 0.68): use 1080×960 panels and crop=1080:960:0:840.
R=output/cartoon-h3/<slug>; T=$R/top; mkdir -p $T
# 1. quote card PNG (text via make_title, plain navy card), 720x640
python3 -c "
from pathlib import Path; from scripts.cartoon_h3.title import make_title
T=Path('$T'); make_title(['<verbatim quote>'], T/'q1.png', size=72, y_frac=0.45)" # no credit line (Fish 10-03)
ffmpeg -v error -y -f lavfi -i color=c=0x2D3452:s=1080x1920 -i $T/q1.png \
-filter_complex "[0][1]overlay=0:0,crop=1080:960:0:480,scale=720:640" -frames:v 1 $T/card1.png
# 2. segments (durations from $R/lines.json start times), same codec params so concat can copy
ffmpeg -v error -y -loop 1 -t <secs> -i $T/card1.png -vf "fps=30,format=yuv420p,setsar=1" -c:v libx264 -crf 18 $T/seg01.mp4
ffmpeg -v error -y -ss <in> -t <secs> -i "<real band clip>" -an \
-vf "scale=720:640:force_original_aspect_ratio=increase,crop=720:640,fps=30,format=yuv420p,setsar=1" -c:v libx264 -crf 18 $T/seg04.mp4
printf "file 'seg01.mp4'\nfile 'seg02.mp4'\nfile 'seg03.mp4'\nfile 'seg04.mp4'\n" > $T/top.txt
ffmpeg -v error -y -f concat -safe 0 -i $T/top.txt -c copy $T/top.mp4
# 3. stack: top panel over the lower-middle of the reactor cut (assembled with --no-end-card), 4 px seam, reactor audio
ffmpeg -v error -y -i $T/top.mp4 -i $R/<slug>-final.mp4 -filter_complex \
"[0:v]setpts=PTS-STARTPTS[t];[1:v]crop=720:640:0:560,setsar=1[b];[t][b]vstack=inputs=2,drawbox=x=0:y=638:w=720:h=4:color=white@0.9:t=fill,format=yuv420p[v]" \
-map "[v]" -map 1:a -c:v libx264 -crf 18 -preset medium -c:a copy -shortest $R/split.mp4
# 4. end card (optional, `--end-card`; off by default per Fish 10-06) over both halves, then preflight
python3 -m scripts.cartoon_h3.endcard $R/split.mp4 $R/split-ec.mp4 --secs 2.5 --text "<end_card>"
python3 scripts/meta_preflight.py $R/split-ec.mp4
- The top track must be at least as long as the reactor cut (
-shortestcuts to the shorter input): pad the last card. - Card colour = brand navy (endcard.py
NAVY2D3452) with the white make_title shape, or a warm off-white card; one look per ad. - Quote text wraps automatically (make_title wraps at 85 % width); if it runs past 3 lines at size 72, pick a shorter verbatim line, don't shrink.
Structure example (ILLUSTRATIVE — shows the shape only, not shipping copy; real card lines come from the corpus via voc_check, real MOM lines through Fish's approval)
Top card 1 (Q, paraphrase placeholder of a stake-bank row): "" (no source line on the card; post id in the top track)
1. [MOM] "A mom posted this last night."
2. [MOM] "