Repository navigation
Conversation
libass has no CSS transform: scaling the active word changed its advance width, so the centred line re-flowed and every other word slid sideways on each word change, and the word settled at 109% while the preview settles at 100%. Each word event is now drawn as two layers from the same text: a base layer with the active word transparent, and a pop layer showing only the active word, which scales about its own centre. Adds scripts/bench-caption-stability.ts, which renders every style through libass and measures word drift against a no-pop render: before 17-26px settled drift and 109.1% active size, after 0px and 100.0% on all 12 styles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QYatd8aXSLCVxPx1SggNZ
Exports are stereo, but loudness was measured on the source layout. A mono source is spread to both channels 3 dB down, so measuring the mono peak sent ordinary speech down the limiter path and limited it about 3 dB harder than needed. Measure and normalise the stereo mix instead. The limiter path applied TARGET_I - inputI and then lost loudness to the limiter, so spiky speech landed short. When a clip needs limiting, re-measure the limited result (audio only, at most two passes) and raise the gain into the same limiter. Adds scripts/bench-loudness.ts: mean error 1.30 LU mono / 0.52 LU stereo before, 0.26 LU both after; true peak <= -2.0 dBTP throughout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QYatd8aXSLCVxPx1SggNZ
Concatenating trimmed audio at arbitrary samples left a waveform step at every join, audible as a click under music, game sound or room tone. Fade 5ms out and 5ms in at each internal join; durations are unchanged, so picture and captions stay in sync. Adds scripts/bench-tighten-joins.ts, which renders the real tighten graph over continuous beds: worst join step was 12.7x / 118x / 14.2x the material's 99.9th percentile step (music, bass, room tone), now 0.2x / 0.6x / 0.2x with zero drift. A duration-preserving crossfade was also measured; it dips less but leaves larger steps and cancels low notes that meet out of phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QYatd8aXSLCVxPx1SggNZ
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this change?
Three export-quality fixes found while comparing Cutawan's pipeline with SupoClip's. Each has a new offline benchmark that renders through the real code path and shows numbers before and after. No API keys are needed and nothing is assumed.
captions.ts). The export popped the active word with\fscx109. In libass that changes the word's advance width, so the centred line re-flows and every other word slides sideways on each word change. The word also settled at 109%, while the preview (a CSS transform from 0.94 to 1) settles at 100%. Each word event is now two layers built from the same text: a base layer with the active word transparent, and a pop layer that shows only the active word. Scaling a single word re-centres the line by half its growth, so that word's own centre doesn't move. SupoClip avoids per-word scaling for the same reason.loudness.ts,render.ts).render.ts). Audio pieces were concatenated at arbitrary samples, leaving a waveform step under any music, game sound or room tone. Each internal join now fades out and back in over 5 ms. No durations change, so picture and captions stay in sync. SupoClip crossfades its joins; a duration-preserving crossfade was measured here too and was worse (see below).Why?
The SupoClip review: on most axes Cutawan's pipeline is already more thorough. Examples:
loudnormand CPU-only encoding.SupoClip has no quality evaluation at all. Its test reports run one 19-second fixture.
The places where SupoClip made a better call were the caption pop and the crossfaded joins. These benchmarks confirmed both were real problems in Cutawan. The loudness issue turned up while measuring.
bench-caption-stability(12 styles): other words' drift, settledbench-loudness: mean error from −14 LUFS, mono / stereobench-tighten-joins: worst join step ÷ material's 99.9th-pct step (music / bass / room)Cross-checks:
loudnorm, run on the same signals, missed by up to 2.8 LU and reached −0.9 dBTP.Release notes
(Also in
CHANGELOG.mdunder Unreleased.)How did you test it?
npm test(730 passed, including new tests that fail on the old code)npm run typechecknpm run lintscripts/test-pipeline.ts,scripts/test-quality.ts,scripts/test-captionsize.tspass--check; numbers aboveAnything to watch out for?
bench-caption-stabilitycan't isolate words for styles whose highlight is the same colour as the other words. For those styles its "during pop" column includes the popping word itself; the settled column is still exact.🤖 Generated with Claude Code
https://claude.ai/code/session_019QYatd8aXSLCVxPx1SggNZ
Generated by Claude Code
Note
Medium Risk
Changes core export paths (ASS caption generation, loudness measurement/normalisation, and tighten audio graphs); limited-path exports may run extra ffmpeg measurement passes, but behaviour is covered by new benchmarks and unit tests.
Overview
This PR tightens export quality in three areas—karaoke captions, loudness mastering, and tighten-cut audio—and adds offline benchmarks so regressions are measurable without API keys.
Karaoke captions (
captions.ts) no longer scale the active word in a single layer (which re-flowed centred libass lines and shifted neighbours 17–26px). Each word now emits two ASS dialogues: a base layer with a transparent active slot, and a pop layer that only draws the highlighted word with a preview-matched 0.94→1 pop over 90ms. The pop runs once per word, not again when events split at layout bands.Loudness (
loudness.ts,render.ts) measures and normalises the stereo mix exports actually carry (fixing mono sources measured ~3 dB too hot before downmix). Clips on the limiter path get up to two extra measurement passes viarefineLimiterGainso peak-limited material still lands near −14 LUFS.Tighten cuts (
render.ts) apply 5 ms fade in/out at internal audio joins only (JOIN_FADE_SEC), removing splice clicks without changing duration or video cuts.tightenGraphis exported for tests.Docs: CHANGELOG and README document the fixes; new scripts
bench-caption-stability,bench-loudness, andbench-tighten-joins(each with--check) exercise the real pipeline paths. Unit tests cover dual-layer captions, stereo measurement, limiter refinement, and join fades.Reviewed by Cursor Bugbot for commit 8cc5bab. Bugbot is set up for automated code reviews on this repo. Configure here.