Repository navigation
Description
Problem
In a narrated video, the voice-over and the captions are made one way:
script → hyperframes tts → word timings (transcribe, or the voice engine's words[]) → the caption script
After that, nothing connects them. The most common review note on a narrated video is "say X instead of Y", whether it comes from the Studio, a timed comment in the desktop app, or a chat with the agent. Acting on it means:
- voicing the line again
- getting new word timings and re-importing the captions (Caption reimport corrupts existing arrays containing ]; #6181 shows that re-import can break an existing caption script)
- re-timing the scene, and everything after it if the length changed
In practice an agent often fixes the caption and forgets the voice, or the other way round, and the mismatch only shows when someone listens to the render. Nothing in check catches it.
Proposed solution
Make each narration line one unit that the composition knows about.
-
One source per line:
{ id, text, voice, speed }, kept with its audio clip, its word timings, its caption words and a hash of the text. For example in anarration.jsonbeside the composition, or asdata-*attributes on the caption group. -
Edit once: changing a line's text (in the Studio, from the CLI, or by the agent):
- re-voices only that line
- replaces its word timings and captions
- moves the scene's timing if the length changed, or flags the overrun
Other lines stay untouched.
-
Check catches drift:
hyperframes checkreports a line whose voice text and caption text no longer match (a stale hash), as "narration out of sync". -
A possible CLI:
hyperframes narration list hyperframes narration set <line-id> "new text" # re-voice, re-caption, re-time this line hyperframes narration check
-
Studio: editing a caption's text offers "Re-voice this line", and a timed comment on a narrated moment can name the line it belongs to.
Alternatives considered
- Fix it by hand or by the agent each time. This is what happens now. It's error-prone, and the gap is invisible until playback.
- Always build captions by transcribing the final mix. The captions are then accurate, but every text change still needs a re-voice and a re-import, so editing stays slow.
- Solve it inside one workflow skill. I'm prototyping this in a proposed community skill, where each step's line drives its voice, captions and guide text. But that only helps that skill. The sync belongs in HyperFrames, so every narrated composition (explainers, demos, launch films with
--voice) gets it.
Additional context
- Related:
- Studio: support image attachments and image-only timed comments #6349: image attachments for timed comments, the review side of this
- Caption reimport corrupts existing arrays containing ]; #6181: caption re-import
- The skill proposal it builds on: Skill proposal: product-demo — a narrated walkthrough and a step-by-step guide from one flow script hyperframes-community-skills#24 (product-demo).
- Offer to help: I'm happy to work on this in core if the design fits, and would welcome guidance first: where should a line's source of truth live (a project file, or the composition HTML), and should the re-timing move later scenes automatically or only warn?
Activity
DhanushMurugesanG commentedon Nov 3, 2026
Thanks for the labels! Here's a concrete design for this, so it's easier to say yes, no or "change X". I won't open any PR for it until the design is agreed, and I'll change it to match what you decide. Separately, while reading the code I found a small bug in the media-use audio engine: a partial
--only ttsrun drops every voice it didn't re-voice. I've sent that as its own small PR: #6816.Proposed design
narration.jsonat the project root, next tocaption-overrides.json. Undo history tracks it there, but wouldn't under.hyperframes/. Each line has:id,text, an optionalvoice/speed/langaudio,durationSecondswords(seconds, relative to the clip)voiced: { textHash, voiceKey, audioHash }, recorded when the line was voicedUnknown keys are kept when the file is rewritten.
Links:
<audio data-narration-line="l2">keeps owning the clip's start and duration.line: "l2", because caption spans are built by script and can't carry attributes. Untagged words fall back to the clip's time window.Lint (a project probe in
packages/lint/src/project.ts, all warning or info, never blocking a render):narration_manifest_invalidnarration_line_unlinkednarration_audio_stale(the text, voice or WAV changed)narration_clip_timing_mismatchnarration_captions_out_of_syncnarration_transcript_mismatch(info)CLI:
hyperframes narration list | init | checkhyperframes narration set <id> ["text"]: re-voice one line into a temp folder, transcribe that clip only (outside the project, sopatchCaptionHtmldoesn't touch the project), splice that line's caption words, update the manifest and the clip's duration, and remapcaption-overrides.jsonindices. The writes go throughapplyFileMutationswithexpectedVersion, as one history entry.--dry-runand--no-retimeare supported.Shared code: goes in
@hyperframes/parsers(./narration,./caption-array), because lint depends only on parsers.PRs, in order:
narration list/init/checknarration setStudio "Re-voice this line" would come last, and only if you want it.
Questions before I open PR 1
narration.json,data-narration-lineon<audio>, andlineon caption words?setsupport:script/TRANSCRIPTwithconst/let/var? IsWORDSin, andGROUPSout?set, or join an agent's open turn? Which label and--who?assets/voice/l2-<hash8>.wav) OK?--only tts?narration checksubcommand, or arelint/checkenough?@hyperframes/parsersthe right home, withfindCaptionArrayand Studio'sextractTranscriptmoved onto it later?voiceover/SCRIPT.mdfrom feat(studio): storyboard frame focus + voiceover iteration #1532?