Repository navigation
bin/vh pace: fixes from the review of #72 - #73
Merged
Merged
Conversation
An independent review of #72 (merged before its findings were in) found that an ASR boundary off by more than about 0.1 s could drop syllables or move one behind a pause, a crash when a line has no audio, a missing GEMINI_API_KEY found only after the take was written, and smaller gaps (--restore --dry-run restored; --ref on a "……" line divided by zero; a 22.05 kHz hop drift; "……" lines collapsed; the .raw timeline rewritten with a transcription). A line's voice is now everything voiced between the silences that part it from its neighbours; the ASR start and end only locate those silences, so nothing voiced can be dropped. Word times follow the audio through atempo's uneven stretch (loudness envelopes aligned by DTW). Keys and mlx-whisper are checked before anything is written. tools/ci.sh covers each case; run against #72's pace.py the new checks fail where the review said.
ZLHad
marked this pull request as ready for review
October 7, 2026 16:08
An independent review of this PR found: - the breath rule peeled every short isolated syllable at a line edge with no loudness test (a line opening with three short loud syllables measured 6.36 for a true 5.07); now one blip per side, 15 dB under the line's speech; - a line with no audio in the middle got zero-width times and an extra clamped pause, and a whispered line was replaced by silence; now such a line stays inside the pause around it, that stretch of the take is copied as it is, the line gets a 0.1 s span and is named; - spans that go back in time (a hand edit) repeated up to 3 s of audio; now refused; - a partial script.txt made a block break the note denied; lines missing from it now count as one block; - CI's 60 ms word bound could not catch a revert of the DTW alignment (now 35 ms: 27 with it, 61 without), and the breath rule, the cache, --target/--extra and the notes had no cases; - the dense DTW matrix took 1.2 GB for one 120 s line (now banded, 0.23 GB); the first transcription was unguarded; an unreachable 50 ms guard. Each new CI case fails against the previous commit and passes now.
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.
Follow-up to #72, which merged (auto-merge) before its independent review came back. This fixes what the review found.
Why
The reviewer found no path that loses the original take, but these defects:
--align geminiwithout a key wrote the take, then exited before the captions (the same for--align whisperwithout mlx-whisper).--restore --dry-runrestored.script.txt(or ids not in it) silently changed the result.--refon a "……" line: ZeroDivisionError..rawtimeline rewritten with the transcription.The
--restorerefusal also suggested deleting the.rawfiles, which may be the only copy of the original.What changed
Line edges (1). The ASR start and end now only locate the silence between two lines: the quiet run nearest the boundary, within 0.2 s of it, at least 0.12 s long when there is one. Shorter dips are between syllables. A line's voice is everything voiced between the silences on either side, so nothing voiced can be dropped. A blip under 0.12 s at a line's edge, set off by 0.06 s of quiet (a breath), stays in the audio but no longer counts as speech in the pace and the pauses. The real take showed why this matters: a breath before a line made it measure 6.40 chars/s instead of 6.93.
Word times follow the audio.
atempostretches pauses and speech by different amounts. The loudness envelopes before and after are now aligned by dynamic time warping, within ±0.15 s of the even stretch. The first word of a line starts, and the last one ends, where the speech does.The other findings:
--restore --dry-runis a usage error (4).script.txtor some ids are missing (5).--refwith no heard line is an error (6).parse_intermixed_args(7).audio/pace.<lang>.asr.json, keyed by the take's sha256. The.rawtimeline stays byte-identical, and--dry-runwrites nothing (10)..rawfiles.Docs: the README section, playbook/04 step 1, the module docstring and the CHANGELOG describe the new edges, the breath rule, "……" lines, the cache,
--restore, and the one case it can't settle: two lines run together with < 0.12 s between them and an ASR boundary off by a syllable can put that syllable in the neighbouring line (nothing is lost).How it was verified
The reviewer's repro scripts, on the synthetic take; syllables lost out of 40 trials:
A boundary 0.1 s inside a syllable moved it behind a 0.39 s pause before; now it stays where it was (0.05–0.2 s tried).
Mapped word starts vs the real onsets on the synthetic take:
The 2-minute, 27-line Gemini take (scratch copy):
bin/vh qaclick warnings: 54, against 57 in the original take.--align whispertranscribes once intopace.zh.asr.json, and a later default run reuses it. The.rawtimeline stays byte-identical, and the pace numbers are the same as with Gemini's word times, since pace is now measured from the waveform.CI:
pace_checksnow uses a 22.05 kHz take with a "……" line, and adds:--restore --dry-run;--align geminiwithout a key (nothing written);--refon a line without words;voiceover.wav/timeline.jsonrestored too.Run against #72's
pace.py, the new checks fail on the "……" pause,--restore --dry-run, the no-audio crash, the keyless write and the--refcrash.tools/ci.sh --committedpasses, also withVH_BASH=/bin/bash(shellcheck and pyflakes through the uv shim).Second review (of this PR) and its fixes (b9ee72a)
An independent Sonnet review of 6d1b18d checked:
--restoreand the cache flow.It found these defects, now fixed:
script.txtmade a block break the note denied--target/--extra/ the language after the flags, and the notes had no CI casesDocumented rather than changed: a whole ASR boundary shifted ≥ 0.4 s one way can put a few syllables in the neighbouring line (nothing is lost); output is mono; a hand-edited paced timeline is rebuilt from
.rawon the next run.Against 6d1b18d's
pace.py, the new CI cases fail: swapped spans not refused, the no-audio middle line at 33 syllables with its pause clamped, the breath case at 6.83 for a true 5.24. Against b9ee72a all pass. The real 27-line take is unchanged by this round (5.88–6.83 chars/s, line starts within 0.032 s of the voice,qaclicks 54).tools/ci.sh --committedpasses, also withVH_BASH=/bin/bash.