Skip to content

embedded-captions: a heroless theme.json silently corrupts the timeline in 18 of 25 themes (2 crash) #3628

Closed

Description

@circleghost

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 authoring

In 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

  1. Guard the two null dereferences (lines 780, 3630).
  2. When hero is 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.
  3. Add a compile-time assertion that the emitted timeline's span matches the composition duration
    (e.g. fail if duration() > 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

  1. circleghost commented on Sep 4, 2026

    @circleghost
    Author

    Related: #3629 (gsap SRI mismatch breaking preview-frames.cjs), found in the same session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions