Skip to content

fix(media-use): keep the other voices when a TTS run re-voices some lines - #6816

Open
DhanushMurugesanG wants to merge 5 commits into
heygen-com:mainfrom
DhanushMurugesanG:fix/media-use-merge-partial-tts
Open

DhanushMurugesanG wants to merge 5 commits into
heygen-com:mainfrom
DhanushMurugesanG:fix/media-use-merge-partial-tts

Conversation

@DhanushMurugesanG

@DhanushMurugesanG DhanushMurugesanG commented Oct 30, 2026 •

Copy link
Copy Markdown

What

With --only, the media-use audio engine (skills/media-use/audio/scripts/audio.mjs) now merges voices by id. A TTS run whose request lists only some lines re-voices those lines and keeps every other voice in audio_meta.json. Before this change, it replaced the whole voices list with the lines from that run. A run without --only still replaces the list, as before.

It also corrects the places that said Kokoro has native word timestamps. HyperFrames' Kokoro path (hyperframes tts) returns none; the engine gets the word timings by running hyperframes transcribe over the WAV.

Why

Steps to reproduce on main:

  1. Run the engine with --only tts and a request that lists lines a, b and c. audio_meta.json has three voices.
  2. Change the text of b, and run again with a request that lists only b.
  3. audio_meta.json now has one voice, b. Voices a and c are gone, though their WAV files are still on disk, and total_duration_s counts only b.

--only is documented as merging into an existing --out, both in the engine header and in skills/media-use/references/audio.md. BGM and SFX already merge. The voice list did not.

Related work

Refs #6708. This is a small, separate fix found while reading the code for that issue. It does not implement linked narration. I searched open and closed issues and PRs for this bug and found no existing report.

Open PRs #5819 and #5702 also edit audio.mjs, inside synthLine, a few lines above this change. The logic does not overlap. Those two and #5568 also change the media-use hash in skills-manifest.json, so whichever lands later needs to regenerate that file.

How

  • New mergeVoices(previous, fresh, { requested, exists }) in skills/media-use/audio/scripts/lib/audio-meta.mjs:
    • If every earlier voice id is in the request, the result is the fresh list, exactly as before: request order, and failed lines are dropped.
    • Otherwise, an earlier voice that was not requested stays in place. A re-voiced line replaces its entry in place, and a line with no earlier voice is appended. A requested line that failed keeps its earlier entry only if that file still exists, so the list never names a missing file.
  • audio.mjs calls mergeVoices where it called results.filter(Boolean), but only when --only is passed. A full run without --only still replaces the list, so removing a line from the request still drops its voice. total_duration_s is computed from the resulting list, and the header comment says when voices merge.
  • The media manifest is still written only for voices made in this run. writtenAssets gets this run's voices, not the merged list. Otherwise the kept voices would be recorded again under this run's provider, with the file name as their description.
  • Kokoro: in packages/cli/src/media-use/lib/local-models.mjs, wordTimestamps is now "transcribe" and the notes text is corrected. The field names where a model's timings come from, as "whisperx" does for fish-speech. The header comment in tts-local-provider.mjs and one assertion message in local-models.test.mjs are corrected too. Nothing reads kokoro's wordTimestamps value at run time.

Test plan

  • Unit tests added/updated
    • skills/media-use/audio/scripts/lib/audio-meta.test.mjs: mergeVoices cases:

      • unrequested voices kept, and a re-voiced line replaced in place
      • a new line appended
      • a failed line dropped when its file is missing, and kept when the file exists
      • a full run returned as synthesized, in request order, with a failed line dropped
    • skills/media-use/audio/scripts/audio.test.mjs: an end-to-end run of the engine, with stub npx (tts and transcribe) and ffprobe on PATH. It voices a, b and c, then re-voices only b, and checks that:

      • a and c stay, and b is replaced in place
      • total_duration_s is the sum of all three
      • the media manifest gains one record, for b
      • a later full run without --only that lists only a and c drops b, as before

      On main this test fails with only b left. It is skipped on Windows, as tts.cleanup.test.mjs is.

    • packages/cli/src/media-use/lib/local-models.test.mjs: assertion message updated.

  • Manual testing performed: not run against real Kokoro. The engine test uses stubs.
  • Documentation updated (if applicable): the engine header comment, and one sentence in skills/media-use/references/audio.md saying how voices merge under --only tts.
  • Comments follow CONTRIBUTING.md "Comments": node scripts/check-comment-citations.mjs and node scripts/comment-ratchet.mjs pass on this diff.

Commands run:

bun run test:skills                  # 616 tests, 0 failures
HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs \
  --test packages/cli/src/media-use/lib/local-models.test.mjs   # 16 tests, 0 failures
bun run lint
bun run format:check
bun packages/cli/scripts/gen-skills-manifest.ts --check

…ines

With --only tts, audio.mjs replaced the whole voices list in
audio_meta.json with the lines from that run, so re-voicing one line
dropped every other voice. Voices now merge by line id under --only.
A run without --only still replaces the list, as before.

Also correct the places that said Kokoro has native word timestamps:
`hyperframes tts` returns none, and the engine runs
`hyperframes transcribe` over the WAV.

Refs heygen-com#6708

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 9a26f780c6a4a561dbe7c9be41c0c193c4c39bd0.

I checked the partial TTS merge, full replacement path and manifest recording. Re-voicing one line retains the other lines and records only fresh assets.

node --test audio-meta.test.mjs audio.test.mjs local-models.test.mjs: 40 passed. Provider subprocesses are stand-ins, no paid TTS calls. Fresh independent source and adversarial controls also passed.

Need: yes · Size: ok · Reuse: existing implementation seam · Simpler: already minimal · Structure: fine
Risk: medium - partial voice metadata persistence.

Verdict: APPROVE
Reasoning: I checked the partial TTS merge, full replacement path and manifest recording. Re-voicing one line retains the other lines and records only fresh assets.

Reasoning: I checked the partial TTS merge, full replacement path and manifest recording. Re-voicing one line retains the other lines and records only fresh assets.

…-partial-tts

# Conflicts:
#	skills-manifest.json
…-partial-tts

# Conflicts:
#	skills-manifest.json
@DhanushMurugesanG

Copy link
Copy Markdown
Author

Thanks for the review, @jrusso1020! skills-manifest.json conflicted with main, so I merged main and regenerated it. That's the only change since your review (the media-use hash); the fix itself is untouched. Could you approve again, and approve the workflow runs so CI can start? This is my first PR here, so they're waiting on a maintainer.

…-partial-tts

# Conflicts:
#	skills-manifest.json
@DhanushMurugesanG

Copy link
Copy Markdown
Author

@jrusso1020 main changed skills-manifest.json again, so I merged main and regenerated it. As before, the media-use hash is the only change since your review. The fix is untouched, and bun run test:skills passes (621 tests). It's ready for your re-approval, and the workflow runs still need a maintainer to approve them. Thanks!

…-partial-tts

# Conflicts:
#	skills-manifest.json
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view to heygen-com#7265

Adopt heygen-com#7284 (Studio Storyboard tab and `doc: "storyboard"` Desktop handoff,
adapted to the fork's desktopApp without heygen-com#6897) and heygen-com#7324 (drawElement gate:
webgl_unavailable / beginframe_no_self_verify). PR axis heygen-com#7260-heygen-com#7265 carries
earlier verdicts; re-pin heygen-com#6695 and heygen-com#6816, resolve heygen-com#7150 as superseded by heygen-com#5746.
Watermarks: commit 18fc75d, PR heygen-com#7265, issue heygen-com#7223, monitored 99.

Co-authored-by: WaterrrForever <miao.yang@heygen.com>
Co-authored-by: Vance Ingalls <vance@heygen.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants