354 lines
30 KiB
Markdown
354 lines
30 KiB
Markdown
---
|
||
tags: [video-stack, spec]
|
||
updated: 2026-10-16
|
||
source: explainer-video-stack/STACK.md
|
||
---
|
||
# STACK.md — Everything Hermes needs to produce Grok-Bot-quality explainer videos
|
||
|
||
Audience: the Hermes **`explainer`** profile on **TonyB** (Windows 11, RTX 3060 12 GB), running a cheap model (e.g. `gpt-6-luna`).
|
||
This is the single source of truth. When another doc disagrees with this one, **this one wins**.
|
||
Repos: `tonyjbala/explainer-video-stack` (business explainers) and `tonyjbala/political-explainer-stack` (ballot/political ads).
|
||
Worked example to copy: `C:\video\examples\prop1ad` (HyperFrames, 45 s, 9:16).
|
||
Last updated: 2026-10-15.
|
||
|
||
---
|
||
## 1. Overview and the quality bar
|
||
|
||
A "Grok-Bot-quality" video is a **30–90 s explainer or ad (default 45 s, 9:16, 1080x1920)** that passes all six tests:
|
||
|
||
| # | Bar | What it means in practice |
|
||
|---|---|---|
|
||
| 1 | **Verified facts** | Every number and claim has an opened, primary source (LAO, official voter guide, SOS, agency data, product docs). Opinion is labelled or phrased as opinion. A short source line sits under every stat on screen. |
|
||
| 2 | **Natural VO** | Written for the ear (Section 5.1). No misreads (Whisper-checked), no odd pauses, no read-aloud stage directions. Political = authoritative male (Gemini **Charon**); business = firm, friendly female (Gemini **Kore**). |
|
||
| 3 | **Music that adds feel** | Timed to the beats: pulse/tension under the problem or cost, a swell under the CTA, resolves on the cut. A generic bed that "doesn't do anything" is a fail (Prop 1 lesson). |
|
||
| 4 | **Kinetic but clean design** | One idea per scene, huge numbers, staggered entrances, a palette made for the topic, nothing inside the platform UI safe zones, no dead half-screens. |
|
||
| 5 | **Perfect captions** | Burned-in, 3-word groups, active word highlighted, timed from Whisper word timestamps, spelled correctly ("$500M", "Prop 1", not "Prop.", not "$5"). |
|
||
| 6 | **Broadcast loudness** | Music ~12 dB under voice with sidechain ducking; final **-16 LUFS integrated, true peak ≤ -1.5 dBTP**, AAC 192k 48 kHz; last word ends ~2 s before the cut. |
|
||
|
||
Plus: three approval gates (script, voice, music) **before** any build, and nothing is ever posted or sent by you.
|
||
|
||
---
|
||
## 2. Hardware and software inventory (verified on TonyB 2026-10-15 unless marked)
|
||
|
||
| Item | Version / path | Notes |
|
||
|---|---|---|
|
||
| GPU | NVIDIA GeForce RTX 3060, **12288 MiB**, driver 610.88 | Qwen alone uses ~6.4 GB baseline (≈11.5 GB with KV in use). TTS models need 3.9–5.0 GB. |
|
||
| LLM | `C:\llama\llama-server.exe`, **Qwen3.8-35B-A3B-Q4_K_M on 127.0.0.1:1922** | The only model the video work assumes. Health: `http://127.0.0.1:1922/health`. |
|
||
| Node | **v24.18.0** (HyperFrames needs Node ≥ 22) | |
|
||
| HyperFrames | `npx --yes hyperframes@0.8.162 <cmd>` (pinned in `hf\ad\package.json`) | Default renderer. Set `$env:HYPERFRAMES_SKIP_SKILLS="1"` so npx doesn't install its bundled agent skills. Render sends anonymous telemetry: run `npx hyperframes telemetry disable` once. Never run `hyperframes publish` (it uploads). |
|
||
| Remotion | `C:\video\template` (16:9 `Main` + 9:16 `MainVertical`, 30 fps) | Fallback renderer only. Copy per project with `xcopy /E /I`. |
|
||
| ffmpeg | **9.0.1 essentials (gyan.dev)** on PATH | Mixing, loudness, muxing. |
|
||
| Chatterbox | venv **`C:\tts\chatterbox`** (uv, Python 3.11, torch 2.6 cu124) → `C:\tts\chatterbox\Scripts\python.exe`; driver `C:\tts\tts.py` | Local fallback voice; also runs Whisper check (`--check-only`). ~1x realtime, 3.9 GB. |
|
||
| Qwen3-TTS | venv **`C:\tts\qwen3tts`** (uv, torch 2.6 cu124, `qwen-tts`) → `C:\tts\qwen3tts\Scripts\python.exe`; 1.7B VoiceDesign + 1.7B Base | Second local option; voice-from-description. Slow (~2.5–5 min per 15–30 s), 4.3–5.0 GB, no flash-attn on Windows. Runner: `C:\tts\vt\vt.py` (env `VREF`/`VTAG`; engines `cb|qwen|qclone`). |
|
||
| Whisper | **faster-whisper `small.en`, int8, CPU** | Built into `tts.py`; produces `words.json` `{"1": [[word,start,end],...]}`. |
|
||
| Kokoro | kokoro-onnx (box/legacy) | Fast fallback voice for drafts only. **Never use a Kokoro clip as a clone reference** (refs in `C:\tts\voices\kokoro_*_ref.wav` are legacy; don't use them). |
|
||
| Human voice ref | `C:\tts\voices\user_announcer_ref.wav` (Anthony, recorded with consent; 20.65 s mono 24 kHz) | The only approved clone reference. |
|
||
| Gemini TTS | `C:\tts\gemini_tts.py` (stdlib only; repo copy `scripts/tts/gemini_tts.py`) | **Default voice engine.** Section 2.1. |
|
||
| **ACE-Step 1.5** | `C:\Users\fight\Projects\ACE-Step-1.5` (v1.5.0, commit ca1e85f, uv `.venv`, torch 2.7.1 cu128). Checkpoints: `acestep-v15-turbo` (DiT), `acestep-5Hz-lm-0.6B` (+1.7B present, not used on 12 GB), `Qwen3-Embedding-0.6B`. Server: schtasks **`ACEStepServer`** → `start_acestep_api.bat` → Gradio UI + REST API on **http://127.0.0.1:7860** (log `acestep_server.log`). Wrapper **`C:\tts\acestep_music.py`** (repo copy `scripts/music/acestep_music.py`) | **Primary music generator (local, free, MIT).** ~22–30 s per 45 s instrumental track on the 3060. Section 2.2. |
|
||
| Music (code, fallback) | `scripts/music/gen.py` (options A–C), `gen2.py` (D–F) in explainer-video-stack; `compose.py` presets in `C:\video\template` | numpy synth, royalty-free. Use only if ACE-Step is down or misses twice. Writes full track + 20 s preview. |
|
||
| Music (paid, optional) | `C:\video\template\el_music.py "<prompt>" <seconds> out.mp3` | ElevenLabs Music, needs `ELEVENLABS_API_KEY`; instrumental only. Optional third choice. |
|
||
|
||
### 2.1 Gemini TTS (`gemini_tts.py`)
|
||
```
|
||
python C:\tts\gemini_tts.py script.json out.wav [--voice Charon] [--style "..."] [--gap 0.2] [--each DIR] [--whole] [--model gemini-3.8-flash-tts] [--fallback-model gemini-3.8-flash-lite-tts]
|
||
```
|
||
- `script.json` = JSON list of lines (or a .txt, one sentence per line). Default: one call per sentence, joined with 200 ms gaps. `--each DIR` also saves `DIR\s01.wav…`.
|
||
- `--whole` = **one API call for the whole script** (use this by default; it saves quota).
|
||
- On a 429 quota error it retries automatically on `gemini-3.8-flash-lite-tts` (`--fallback-model ""` disables).
|
||
- **Presets (user-approved Oct 2026):**
|
||
- Political (sample P1): `--voice Charon` (default style "measured, authoritative newscaster, confident, serious").
|
||
- Business (sample B6): `--voice Kore --style "friendly, polished product explainer, conversational, upbeat but credible" --whole`.
|
||
- **Quota (free tier):** flash-tts = **10 requests/day**; flash-lite-tts = 3 requests/minute. A per-sentence political ad can eat a whole day. Paid tier costs about $0.01–0.03/min.
|
||
- **Key:** `GEMINI_API_KEY` from the environment. Put `GEMINI_API_KEY=...` in `C:\Users\fight\AppData\Local\hermes\profiles\explainer\.env` (or `setx GEMINI_API_KEY "..."`). Never print it, echo it, or commit it.
|
||
- Output: 24 kHz mono 16-bit WAV.
|
||
|
||
---
|
||
### 2.2 ACE-Step 1.5 music (`acestep_music.py`)
|
||
- **What:** open-source text-to-music model (ACE-Step 1.5, turbo DiT + 0.6B planning LM). Code and weights are **MIT licensed** (`LICENSE`, `pyproject.toml`, checkpoint model card) → commercial use OK, output is ours. Still: never prompt "in the style of <artist>", never upload copyrighted reference audio.
|
||
- **Runs as:** a persistent local server (Gradio UI for humans + REST API for scripts) on `127.0.0.1:7860`, launched with `--enable-api --init_service true --config_path acestep-v15-turbo --lm_model_path acestep-5Hz-lm-0.6B --offload_to_cpu true`. Check: `curl http://127.0.0.1:7860/health` → `"status":"ok"`. If down: `schtasks /Run /TN ACEStepServer` (models load in ~80 s). Do not start a second copy.
|
||
- **VRAM coexistence:** it shares the 3060 with the one Qwen llama-server (port 1922). Idle ~8.5–10 GB total, **peak ~11.7 / 12 GB during generation** (CPU offload is on). It is not an LLM, so it does not break the one-model rule, but: never run ACE-Step generation at the same time as Qwen3-TTS or Chatterbox (OOM risk) — generate music, then voice, one job at a time. Never start Gemma or a second llama-server.
|
||
- **Command (one track = full + 20 s preview, -18 LUFS mp3):**
|
||
```powershell
|
||
$AP="C:\Users\fight\Projects\ACE-Step-1.5\.venv\Scripts\python.exe"
|
||
& $AP C:\tts\acestep_music.py --prompt "serious civic political ad underscore, cinematic orchestral, low strings, steady timpani pulse, building to a resolute swell, no vocals" --duration 47 --bpm 80 --key "D minor" --out C:\video\myproj\music\optA
|
||
# -> optA.mp3 (full, -18 LUFS two-pass loudnorm), optA_preview.mp3 (last 20 s; --preview-start N to choose), optA_raw.wav (use raw/full in the mix)
|
||
# options: --seed N (repeatable), --steps 8 (turbo default), --thinking (LM planning; slower), --lufs -18
|
||
```
|
||
Prints JSON with `gen_seconds`. Always instrumental (sends `lyrics="[Instrumental]"`).
|
||
- **Prompt tips (ACE-Step expects comma-separated tags, not sentences):** genre/use, mood, instruments, tempo feel, arc, then `no vocals`. e.g. business: `modern, warm, upbeat corporate tech, light synth pulse, piano, no vocals` (`--bpm 110`); political: as above (`--bpm 70-90`, minor key). Put BPM/key in the flags, not the prompt. Keep it to 6–12 tags. For 3 options at Gate 3, vary the tags (e.g. strings vs piano vs brass) or seeds. It doesn't follow exact timestamps, so pick the take whose swell lands near the CTA, or trim/offset the start so it does. Seconds-level timing still comes from the edit.
|
||
- **Measured (2026-10-16):** 45 s track in 22–30 s wall time; output 48 kHz stereo.
|
||
|
||
## 3. The end-to-end pipeline (with gates)
|
||
|
||
```
|
||
Brief → Research/fact-check → Script (2 variants) → [GATE 1 script]
|
||
→ Voice sample → [GATE 2 voice] → Music previews x3 → [GATE 3 music]
|
||
→ Full VO + Whisper check → Scene design → HyperFrames build + lint + stills
|
||
→ Mix → Final QA → Deliver (9:16 mp4, plus 16:9 re-layout if asked) → [GATE 4 final approval]
|
||
```
|
||
Project folder: `C:\video\<project>\` with `brief.md`, `vo_review.md`, `script.json`, `vo\`, `music\`, `hf\ad\`, `out\`, `snaps\`.
|
||
|
||
### Step 1 — Brief → `brief.md`
|
||
Topic, audience, length (default 45 s), aspect (9:16 first), sponsor / "Paid for by" name, stance (political), CTA, tone, deadline.
|
||
Political: use `political-explainer-stack\templates\brief.md`.
|
||
**Done when:** every field filled or explicitly "ask user".
|
||
|
||
### Step 2 — Research and fact-check → `vo_review.md`
|
||
- Open every source you cite. Prefer primary: LAO ballot analysis, CA SOS Quick Guide / Official Voter Guide, UC Berkeley IGS, agency data, the product's own docs.
|
||
- Table columns: beat | original line | suggested accurate line | status (✅/⚠️) + why | source URL. Then a "Sources" list.
|
||
- Watch for the Prop 1 traps: a figure that applies to only part of the measure ($500–600M/yr is for the $10B part only), "inflation-adjusted" qualifiers, endorsements you cannot verify (EDC GOP's own slate was not online; attribute only to a guide you actually have).
|
||
- Label opinion lines ("doesn't change the rules that limit supply").
|
||
**Done when:** every number has a URL, every ⚠️ has a fix.
|
||
|
||
### Step 3 — Script → `script.json` (2 variants)
|
||
Rules in Section 5.1. Structure (political, 5 beats): **hook (0–7 s) → breakdown → cost/stakes → concerns/proof → vote call**. Business: hook → problem → how it works → proof/benefit → CTA.
|
||
~**105 words max for 45 s** (≤150 wpm, ~2 s spare). One JSON string per beat/scene. Numbers spelled as spoken.
|
||
**GATE 1:** send both variants (plain text, with word counts and estimated seconds). Stop until the user picks or edits.
|
||
|
||
### Step 4 — Voice sample → **GATE 2**
|
||
Render line 1 (or the first ~15 s) with the preset voice (Section 4.4). If the user wants options, render 2–3 voices of the same line.
|
||
**Done when:** the user approves one voice by name.
|
||
|
||
### Step 5 — Music previews → **GATE 3**
|
||
Write a timed music brief first (e.g. "0–6 s sparse tension; 15–30 s driving low pulse under the cost; 38–45 s resolute swell, resolve on the cut"). Make **3 distinct options** with **ACE-Step** (code synth only as fallback), 20 s previews of the most important section, mp3. One-line description each.
|
||
**Done when:** the user picks one (or asks for changes; iterate).
|
||
|
||
### Step 6 — Full VO + Whisper check → `vo\v1..vN.wav`, `vo\words.json`
|
||
Render the whole script with the approved voice, one file per line (or `--whole` then Whisper). Check transcript vs script; re-roll any misread or awkward line.
|
||
**Done when:** every line matches (digit-vs-word differences are fine), no pause > ~0.6 s inside a sentence, total VO ≤ length − 2 s.
|
||
|
||
### Step 7 — Scene design
|
||
Pick palette, type, motion for **this topic** (Section 5). Plan one scene per VO line, with start = line start − 0.1 s.
|
||
Safe zones 9:16: **top 250 px, bottom 430 px, right 90 px** clear. Content box x 80–910, y 380–1260. Captions at y≈1350.
|
||
|
||
### Step 8 — HyperFrames build
|
||
Scene-per-`<div>` pattern from `prop1ad\build.py`: one Python script writes `hf\ad\index.html` from `words.json` + scene HTML. Lint, snapshot stills at key timestamps, look at them, fix, then render silent.
|
||
**Done when:** lint clean, stills at hook / each scene / final 2 s look right, `out\silent.mp4` is exactly the target length.
|
||
|
||
### Step 9 — Mix
|
||
Exact ffmpeg in Section 4.9. **Done when:** I = -16 ±1 LUFS, TP ≤ -1.5 dBTP, voice clearly on top, music audible in gaps and swelling at the CTA.
|
||
|
||
### Step 10 — Final QA → deliver → **GATE 4**
|
||
Run the checklist (Section 4.10). Deliver `out\<project>_45s_vertical.mp4` (H.264 + AAC). If a 16:9 version is wanted, **re-lay out** the scenes for 1920x1080 (new positions, side-by-side panels); never just crop or pad the vertical. Send the path + report to the user; never post it.
|
||
|
||
---
|
||
## 4. Exact commands (PowerShell on TonyB)
|
||
|
||
### 4.0 Setup per project
|
||
```powershell
|
||
$P = "C:\video\myproj"
|
||
New-Item -ItemType Directory -Force $P,"$P\vo","$P\music","$P\out","$P\snaps" | Out-Null
|
||
Copy-Item -Recurse C:\video\examples\prop1ad\hf "$P\hf" # HyperFrames project skeleton
|
||
Copy-Item C:\video\examples\prop1ad\build.py "$P\build.py" # then edit scenes/palette
|
||
$env:HYPERFRAMES_SKIP_SKILLS = "1"
|
||
# Preflight: exactly one llama-server, on 1922
|
||
Get-CimInstance Win32_Process -Filter "Name='llama-server.exe'" | % { "$($_.ProcessId) $($_.CommandLine)" }
|
||
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
|
||
```
|
||
Done: one llama-server line (port 1922). If you see more, **report it to the user; don't stop anything yourself.**
|
||
|
||
### 4.1 Long jobs (anything > ~2 min: Qwen3-TTS, Chatterbox full runs, renders)
|
||
Remote-shell children die on disconnect, so use a one-time scheduled task:
|
||
```powershell
|
||
schtasks /create /tn OneTime_VO /tr "cmd /c C:\video\myproj\run_vo.cmd > C:\video\myproj\vo\log.txt 2>&1" /sc once /st 23:59 /it /f
|
||
schtasks /run /tn OneTime_VO
|
||
# poll: Get-Content C:\video\myproj\vo\log.txt -Tail 5 (until it prints DONE)
|
||
schtasks /delete /tn OneTime_VO /f
|
||
```
|
||
|
||
### 4.4 Voice
|
||
```powershell
|
||
cd $P
|
||
# Political sample (Gemini Charon), line 1 only:
|
||
python C:\tts\gemini_tts.py sample.json vo\sample.wav --voice Charon --whole
|
||
# Business sample (Gemini Kore):
|
||
python C:\tts\gemini_tts.py sample.json vo\sample.wav --voice Kore --style "friendly, polished product explainer, conversational, upbeat but credible" --whole
|
||
# Full VO, one file per line (political; costs N requests):
|
||
python C:\tts\gemini_tts.py script.json vo\voice.wav --voice Charon --each vo
|
||
# Local fallback (no key / quota gone): Chatterbox with the consented human ref, one line per call
|
||
C:\tts\chatterbox\Scripts\python.exe C:\tts\tts.py script.json vo --ref C:\tts\voices\user_announcer_ref.wav --ex 0.55 --cfg 0.4
|
||
# Re-roll one bad line:
|
||
C:\tts\chatterbox\Scripts\python.exe C:\tts\tts.py script.json vo --ref C:\tts\voices\user_announcer_ref.wav --ex 0.55 --cfg 0.4 --only 3 --seed 22
|
||
```
|
||
Whisper check / words.json (works on any WAVs named `v1.wav..vN.wav`; rename Gemini's `s01.wav` → `v1.wav` first):
|
||
```powershell
|
||
Get-ChildItem vo\s*.wav | % { $n=[int]($_.BaseName.Substring(1)); Copy-Item $_.FullName "vo\v$n.wav" }
|
||
C:\tts\chatterbox\Scripts\python.exe C:\tts\tts.py script.json vo --check-only
|
||
```
|
||
Done: every HEARD line matches WANT (digits vs words OK). Join lines with 200 ms silence for `vo\voice.wav` if you rendered per line (gemini_tts.py does this already).
|
||
|
||
### 4.5 Music previews
|
||
```powershell
|
||
# PRIMARY: ACE-Step (Section 2.2), 3 options with different tags/seeds
|
||
$AP="C:\Users\fight\Projects\ACE-Step-1.5\.venv\Scripts\python.exe"
|
||
foreach($o in @(@("A","<tags A>, no vocals"),@("B","<tags B>, no vocals"),@("C","<tags C>, no vocals"))){ & $AP C:\tts\acestep_music.py --prompt $o[1] --duration 47 --out C:\video\myproj\music\opt$($o[0]) }
|
||
# FALLBACK code-synth options (edit the arc/section times to the brief first)
|
||
cd C:\video\myproj\music
|
||
python <explainer-video-stack>\scripts\music\gen.py # A–C
|
||
python <explainer-video-stack>\scripts\music\gen2.py # D–F
|
||
# or presets: $env:MUSIC_SECONDS="47"; C:\tts\chatterbox\Scripts\python.exe C:\video\template\compose.py civic_tension cinematic_build
|
||
# or ElevenLabs (if key set): C:\tts\chatterbox\Scripts\python.exe C:\video\template\el_music.py "dark low strings, slow heartbeat pulse, builds to resolute swell at 38s" 47 optA.mp3
|
||
ffmpeg -y -i optionA_full.wav -ss 25 -t 20 -af "afade=t=in:d=0.5,afade=t=out:st=19:d=1" -b:a 160k optionA_preview.mp3
|
||
```
|
||
Music length = video length + ~2 s.
|
||
|
||
### 4.8 Build, lint, stills, render (HyperFrames)
|
||
```powershell
|
||
cd $P
|
||
python build.py # writes hf\ad\index.html
|
||
cd hf\ad
|
||
npx --yes hyperframes@0.8.162 lint
|
||
npx --yes hyperframes@0.8.162 snapshot --at 1,5,12,24,35,43 -o ..\..\snaps
|
||
# LOOK at every PNG in snaps\ (Read tool). Fix, rebuild, re-snapshot only what changed.
|
||
npx --yes hyperframes@0.8.162 render -o ..\..\out\silent.mp4 -q delivery
|
||
ffprobe -v error -show_entries format=duration:stream=width,height -of csv=p=0 ..\..\out\silent.mp4
|
||
```
|
||
Done: lint 0 errors; stills OK; 1080x1920; duration = target (e.g. 45.0).
|
||
|
||
### 4.9 Mix (exact command; same as `scripts/mix.sh`)
|
||
```powershell
|
||
cd $P
|
||
ffmpeg -v error -y -i vo\voice.wav -i music\approved.wav -filter_complex "[1:a]volume=-12dB[m];[0:a]asplit=2[v][sc];[m][sc]sidechaincompress=threshold=0.05:ratio=6:attack=20:release=300[md];[v][md]amix=inputs=2:duration=longest:normalize=0,loudnorm=I=-16:TP=-1.5:LRA=11[a]" -map "[a]" -ar 48000 out\mix.wav
|
||
ffmpeg -v error -y -i out\silent.mp4 -i out\mix.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 192k -ar 48000 -shortest out\myproj_45s_vertical.mp4
|
||
ffmpeg -hide_banner -i out\myproj_45s_vertical.mp4 -af ebur128=peak=true -f null - 2>&1 | Select-String "I:|LRA:|Peak:" | Select-Object -Last 3
|
||
```
|
||
If the VO doesn't start at 0 s, delay it first: `-af "adelay=200|200"` on the voice or build `voice.wav` with the exact line offsets used in `build.py` (`ST` list).
|
||
Done: I -16 ±1, Peak ≤ -1.5, listen once end to end.
|
||
|
||
### 4.10 Final QA checklist
|
||
- [ ] Every fact sourced; source line on screen; "Paid for by …" + "Demo, not official" if political/demo.
|
||
- [ ] Gates 1–3 approved (script, voice, music).
|
||
- [ ] Whisper matches script; listened end to end; no awkward pauses.
|
||
- [ ] Captions match audio word for word, 3-word groups, active word highlighted, inside safe zones.
|
||
- [ ] Exact duration; last word ends ≥ ~2 s before the cut; music resolves on the cut.
|
||
- [ ] -16 LUFS ±1, TP ≤ -1.5 dBTP; music audible but never masking VO.
|
||
- [ ] H.264 + AAC, 1080x1920, file name ends in `.mp4`, plays on a phone.
|
||
- [ ] Exactly one llama-server (1922) still running; no leftover scheduled tasks.
|
||
- [ ] Score the run in `scorecard.md` (copy, voice, music, visuals, speed, cost; 1–5).
|
||
|
||
---
|
||
## 5. Design system
|
||
|
||
### 5.1 Copywriting (write for the ear)
|
||
1. Sentences ≤ 12 words. One idea per line; one line = one breath.
|
||
2. Numbers as spoken: "ten billion dollars", "twenty twenty-six", "Prop One". Never two numbers in one sentence if avoidable (Prop 1 line 3 had four → awkward).
|
||
3. No comma chains or read-aloud lists; split them.
|
||
4. Contractions (it's, don't, you'll). Lead with the stake in the first 2 s.
|
||
5. 1 s breath before the CTA. ≤150 wpm; ~105 words per 45 s.
|
||
6. Two variants every time. Read aloud (TTS) and time against words.json before designing.
|
||
|
||
### 5.2 Palette, type, motion — chosen per topic
|
||
- Start from the topic's emotion: civic/serious (navy, red, cream), local/warm (greens, sand), tech/fintech (deep ink + one electric accent). One dark base, one light text, **one accent**, one highlight color for the active caption word. Check contrast (white on base ≥ 7:1).
|
||
- Type: one heavy sans (`"Liberation Sans", Arial` weight 800–900 for numbers/headlines, 700 for subs). Sizes for 1080x1920: kicker 30 px letter-spaced caps; sub 46 px; hero numbers **128–280 px**; notes/sources 26–28 px (≥26 px).
|
||
- Motion (GSAP): each scene has `a1/a2/a3` elements entering at **+0.2 / +0.7 / +1.2 s**, `opacity 0→1, y 40→0, 0.5 s, power2.out`. No bounces or spins for political. Business can add a count-up or slide, still short (≤0.6 s).
|
||
- Never reuse Prop 1's look unchanged for another topic.
|
||
|
||
### 5.3 Captions
|
||
Built from Whisper `words.json`: group **3 words**, show from first word start to min(next group start, last word end + 0.4 s); active word tweens to the highlight color (`#FFD34D` in Prop 1) over 0.08 s at its timestamp. 60 px, weight 800, centered, `text-shadow:0 4px 12px #000`, box x 80–910 at y 1350. Clean tokens: "Prop." → "Prop", fix currency splits ("$5"+"00" → "$500"), attach "%"/"." to the previous token.
|
||
|
||
### 5.4 Kinetic numbers and data callouts
|
||
One number per beat, hero size; proportional bars (`$10B` 820 px vs `$1.25B` 103 px); `stat` blocks with a 10 px accent rule; "✕" concern cards (max 3); a source note under each.
|
||
|
||
### 5.5 End cards
|
||
Held ≥ 5 s. Political: big **NO/YES** block in the accent color, "ON PROP N", sponsor, close line, bordered box "Paid for by <exact committee name> · Demo, not official / Source: …". Business: logo/name, one-line value, CTA (URL), no clutter.
|
||
|
||
### 5.6 Political vs business
|
||
| | Political | Business |
|
||
|---|---|---|
|
||
| Voice | Gemini **Charon**, authoritative male | Gemini **Kore**, firm friendly female |
|
||
| Music | dark/urgent or resolute; pulse under cost, swell under vote call | warm/tech pulse, upbeat but not cheesy |
|
||
| Look | civic navy/red/cream, flag stripe, no playful motion | topic-fitted brand palette, livelier motion OK |
|
||
| Compliance | "Paid for by", demo label, FPPC check before any real use | product claims sourced |
|
||
| Repo | political-explainer-stack | explainer-video-stack |
|
||
|
||
---
|
||
## 6. Hard rules
|
||
1. **One llama-server only: Qwen on 1922.** Never start Gemma (1923) or any second model alongside. Don't stop or start models without asking the user. (Don't run `C:\llama\restore_llama_servers.bat` — the TonyB copy still starts Gemma too.)
|
||
2. **Long runs via one-time `schtasks`**, then delete the task.
|
||
3. **Never post, send, publish, upload or share** without explicit approval. No `hyperframes publish`.
|
||
4. **Gitea: tonyjbala account only.** Never use Ty / Ty_Tech credentials.
|
||
5. **No Kokoro-made clone references.** Clone only from `C:\tts\voices\user_announcer_ref.wav` (consented) or documented public-domain audio. Never clone an identifiable person without written consent.
|
||
6. **Keep tokens low:** reuse templates, read files only as needed, stills instead of test renders, regenerate only the broken line.
|
||
7. Never print or commit keys (`GEMINI_API_KEY`, `ELEVENLABS_API_KEY`); no media files (mp4/wav/mp3) in git.
|
||
|
||
---
|
||
## 7. Known pitfalls and fixes
|
||
| Pitfall | Fix |
|
||
|---|---|
|
||
| **Blank HyperFrames scenes** — scenes render empty because the GSAP selector doesn't match (e.g. targeting `.a1` without the scene id, or an id that doesn't exist), so elements stay at the CSS start state / are never animated in. | Every scene div needs a unique `id="sN"` + `class="scene clip"` + `data-start`/`data-duration`; animate with `"#sN .a1"`. Register `window.__timelines["main"]=tl` and `tl.set({},{},<duration>)`. Run `lint`, then `snapshot` mid-scene to confirm content is visible. |
|
||
| **Gemini reads directions aloud** ("Say in a calm voice: …" gets spoken) | Text = only words to be spoken. Delivery goes in `--style` (sent as `speech_metadata.style`). systemInstruction is rejected. |
|
||
| **Gemini quota (429)** — 10/day on flash-tts | Use `--whole`; script auto-falls back to Flash-Lite (3/min). The daily quota resets once a day (observed around 4:50 PM PT). Fallback: Chatterbox with human ref. |
|
||
| **Whisper number formatting** ("350,000" vs "three hundred fifty thousand") | Not a misread. Compare normalized; for captions show digits (they read better on screen). |
|
||
| **Awkward VO** (odd pauses, flat delivery) | Shorter sentences, fewer numbers per line; one sentence per call; re-roll seed; never a Kokoro reference. |
|
||
| **Tight ending** (slower voice runs into the cut) | Budget ~2 s spare; trim words, or speed VO ≤3% (`atempo=1.03`); never cut the last word. |
|
||
| **Windows mp4 extension** — Explorer hides extensions, so a file can end up `name.mp4.mp4` or without `.mp4` and won't play on the phone | Always pass an explicit `-o ...\name.mp4`; verify with `Get-ChildItem out` (shows full names) and `ffprobe`. |
|
||
| HyperFrames npx installs skills / telemetry | `$env:HYPERFRAMES_SKIP_SKILLS="1"`; `npx hyperframes telemetry disable`. |
|
||
| Remote shell jobs die on disconnect | schtasks (Rule 2). |
|
||
|
||
---
|
||
## 8. How a cheap model should work
|
||
1. **One small step at a time.** Do one numbered step from Section 3, write its output to a file, then verify it (read the file back, run the check command) before the next.
|
||
2. **Use the "Done when" lines and the QA checklist literally.** If a check fails, fix only that thing.
|
||
3. **Copy, don't invent.** Start from `prop1ad\build.py`, the templates, and the exact commands above; change values, not structure.
|
||
4. **Stop at every GATE** and ask the user in one short message with the files/paths. Don't build ahead of approvals.
|
||
5. **Small context.** Don't paste whole files or logs; use `-Tail`, `Select-String`, and stills.
|
||
6. **When unsure, ask.** If a source can't be opened, a tool errors twice, or VRAM is short: report it and ask; don't improvise around a rule.
|
||
7. **Report at the end:** mp4 path, duration, LUFS/TP, voice + settings, final script, sources, claims softened, scorecard.
|
||
|
||
---
|
||
## Appendix — file map
|
||
- `C:\tts\` — `gemini_tts.py`, `tts.py` (Chatterbox + Whisper), `gen3060.py`, `chatterbox\`, `qwen3tts\`, `vt\vt.py`, `voices\user_announcer_ref.wav`
|
||
- `C:\video\` — `examples\prop1ad`, `template` (Remotion), `voice_tests` (B1–B10, P1–P4, NOTES.md), `COPYWRITING.md`, `HERMES_PROMPT.md`, `STACK.md` (this file)
|
||
- Hermes profile — `C:\Users\fight\AppData\Local\hermes\profiles\explainer\` (`SOUL.md`, `STACK.md`, `.env`, `skills\media\explainer-video\SKILL.md`)
|
||
- Obsidian — `C:\Users\fight\Documents\Obsidian Vault\Video Stack\Full Stack Spec.md`
|
||
|
||
|
||
## Prop 39 critique: lessons (2026-10-16)
|
||
|
||
Source: Hermes's "Yes on 39" 1:38 16:9 draft (`C:\video\prop39`). Length 1:38 was intended — not a defect.
|
||
|
||
### 1. Captions lost every space ("areyou?Many", "governmentissuedID")
|
||
**Root cause:** `C:\video\prop39\build_hf.py` line 35 built word spans separated by a space, but the `.cap` CSS rule (line 171) was `display:flex;justify-content:center`. In a flex container each `<span>` becomes a flex item and whitespace-only text between items is discarded, so the words render glued together.
|
||
- [ ] `.cap` is `display:block` (never flex/grid). Center with `text-align:center`.
|
||
- [ ] Word spans joined with `' '.join(...)` — real space text nodes.
|
||
- [ ] Run `python scripts/qa/check_captions.py hf/ad/index.html vo/words.json` — must print `CAPTION CHECK OK`.
|
||
- [ ] Look at 3 stills with captions on screen and read them aloud.
|
||
|
||
### 2. Safe zones
|
||
- 16:9 1920x1080: title-safe 5% → 96px left/right, 54px top/bottom. Nothing (DRAFT tag, logos, header) outside it.
|
||
- Captions bottom edge ≥ 12% from the bottom (≥130px at 1080p), so player bars don't cover them.
|
||
- 9:16 1080x1920: top 250px, bottom 430px, right 90px are UI zones. Content and captions stay inside.
|
||
- [ ] Export stills of first scene, a caption-heavy scene and end card; check against the zones.
|
||
|
||
### 3. Voice was flat (uniform pauses)
|
||
- Political = Gemini **Charon**; business = **Kore**. Steer delivery in `--style`, never in the text.
|
||
- Natural punctuation, contractions ("it's", "don't"), mix short and long sentences.
|
||
- Vary gaps between sentences 150–400ms (longer before the big point, shorter in lists). Never one fixed gap.
|
||
- [ ] Listen to the whole VO once, start to finish, before building visuals.
|
||
|
||
### 4. Persuasive copy — pick the mode first
|
||
- Decide: **neutral explainer** or **advocacy ad**. Write it in the brief and stick to it.
|
||
- Advocacy ("Yes on X") argues from line 1. No hedges like "other access concerns remain debated" or "laws vary" — those belong only in neutral explainers.
|
||
- Lead with the strongest lines (e.g. "When the process feels loose, people stop trusting it", "A secure election is a fair election").
|
||
- Cut procedural scenes (e.g. "best-efforts" data/reporting) that kill momentum.
|
||
|
||
### 5. Pacing
|
||
- 10–12s static slides feel like a deck. Something visible changes every 3–5s: a build, kinetic number, highlight, camera push, or cut.
|
||
- [ ] No scene element is static >5s (check the timeline).
|
||
|
||
### 6. Facts and disclosure
|
||
- Every stat (e.g. "36 states", Gallup/Pew numbers) has a source URL in `vo_review.md` / `sources.md`.
|
||
- No chart labeled "illustrative" presented as data: use real numbers with citation, or remove.
|
||
- A real "Paid for by ..." line replaces any "sponsor disclosure pending" placeholder before publishing.
|
||
|
||
## Pre-render QA checklist (Hermes must run all, every video)
|
||
1. [ ] **Captions:** `check_captions.py` passes; 3 stills read correctly with spaces.
|
||
2. [ ] **Safe zones:** stills checked (16:9: 96/54px, captions ≥12% up; 9:16: 250/430/90).
|
||
3. [ ] **Voice:** full listen; varied pauses; Whisper check has no misreads.
|
||
4. [ ] **Facts:** every number/claim has a source in `vo_review.md`; no "illustrative" data.
|
||
5. [ ] **Mode:** brief says neutral or advocacy; no copy contradicts it.
|
||
6. [ ] **Pacing:** visual change every ≤5s.
|
||
7. [ ] **Disclosure:** real "Paid for by" end card (political); DRAFT tag inside safe zone or removed for final.
|
||
Report each item's result to the user before delivering.
|
||
|