Repository navigation
Description
Summary
SKILL.md and scripts/make-theme.cjs:238 both document theme.hero as optional:
// theme.hero is OPTIONAL: a heroless theme runs pure-body (the right authoringIn practice, omitting hero is only safe for 4 of the 25 shipped themes. In 18 of them the
generated timeline is silently corrupted — the render exits 0, every gate passes, and the
captions simply freeze on the first line or two for the entire video. Two more crash at compile.
Environment
| hyperframes CLI | 0.8.26 (checkout built from main, bun install && bun run build) |
| Node | v22.17.0 |
| OS | macOS 25.6.0, Apple M4 Pro |
| Skill files | ~/.claude/skills/embedded-captions (as installed by hyperframes skills update) |
Project used for all measurements: a 65.5 s single-shot 1920×1080 talking-head clip, 32 caption
lines, prepare.sh run normally (transcript.json, frames_fg/, safe-zones.json, matte.fps
all present).
Root cause
scripts/make-theme.cjs:293 (and the same sentinel again at line 297):
const heroIn = hero ? hero.start : -999;-999 is used as a "no hero" sentinel, but the paradigm emitters schedule GSAP tweens at times
derived from heroIn. With no hero, tweens are emitted at absolute timeline positions near
-999. GSAP then spans the timeline from that negative position, which both inflates
duration() and shifts the time base, so tl.seek(t) no longer lands on second t of the
composition.
Grep of the generated rail.html confirms the sentinel reaches the output:
vhs -999.000
chalkboard -999.070
scoreboard -999.000 -999.070
thunder -999.000 -999.042 -999.083 -999.110
Measurement
Same project, same 32 lines, only dna changed, no hero key. Timeline duration read in
headless Chrome as window.__timelines['main'].duration(). Expected ≈ 65.5 s.
| Result | Themes | duration() |
|---|---|---|
| Correct | anchor, neonsign, inkwater, stardust |
63.7 – 65.6 s |
| Silently corrupted | ordnance, hologram, vhs, chalkboard, scoreboard, transit, arcade, aurora, biolume, brush, dossier, graffiti, laser, papercut, popup, ransom, stomp, thunder |
999 – 51 001 s |
| No timeline registered | lastpage |
— |
| Crashes at compile | terminal, spectrum |
see below |
Selected values: ordnance 29 034.91 · thunder 51 001.17 · vhs 20 042.83 ·
chalkboard 13 044.60 · dossier 13 051.65 · stomp 998.96.
The two crashes
Both dereference hero without a null guard, reached only when hero is absent:
scripts/make-theme.cjs:780 redactLinkage && w.start >= hero.start - 0.01 …
→ TypeError: Cannot read properties of null (reading 'start') [terminal, paradigm `panel`]
scripts/make-theme.cjs:3630 for (let k = 0; k < hero.len; k++) {
→ TypeError: Cannot read properties of null (reading 'len') [spectrum, paradigm `scoperail`]
Why the silent cases are worse than the crashes
The 18 corrupted themes exit 0 and pass every downstream gate: overflow(rail) PASS,
rail-climax PASS, no PAGEERROR. render-theme.sh reports success. The only symptom is
frozen captions for the whole render — you have to sample frames to notice, and the PAGEERROR
gate (which exists precisely to catch silently-blank caption layers) does not fire.
Confirmation that the missing hero is the trigger
Re-running the identical project with a hero.match added (and the hero word removed from
lines for embed setpieces) restores every one tested:
| Theme | heroless | with hero |
|---|---|---|
vhs |
20 042.83 s | 65.53 s |
chalkboard |
13 044.60 s | 63.50 s |
scoreboard |
3 062.16 s | 65.43 s |
transit |
5 057.94 s | 65.51 s |
hologram |
2 063.61 s | 65.52 s |
Suggested fixes
- Guard the two null dereferences (lines 780, 3630).
- When
herois absent, skip emitting hero-derived tweens entirely rather than emitting them at
heroIn = -999. A cheap belt-and-braces alternative is to clamp every emitted tween position
to>= 0. - Add a compile-time assertion that the emitted timeline's span matches the composition duration
(e.g. fail ifduration() > compositionDuration * 1.5). That single check would have caught
all 18 silent failures.
Workaround
Always supply a hero, or restrict yourself to anchor / neonsign / inkwater / stardust.
Activity
circleghost commentedon Sep 4, 2026
Related: #3629 (gsap SRI mismatch breaking
preview-frames.cjs), found in the same session.