Skip to content

fix(stt): re-anchor whisper's word boundaries on the audio, so transcript trims cut where the words are#172

Merged
EtienneLescot merged 2 commits into
release/1.8.0from
claude/transcript-trim-precision-01625f
Jul 27, 2026
Merged

fix(stt): re-anchor whisper's word boundaries on the audio, so transcript trims cut where the words are#172
EtienneLescot merged 2 commits into
release/1.8.0from
claude/transcript-trim-precision-01625f

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Deleting words in the transcript editor produced a trim whose edges were audibly wrong every time: the attack of the first removed word stayed in, and the cut bit into the following kept word.

The editor is not at fault — it turns a selection into exactly [firstWord.startSec, lastWord.endSec] (RightPanes.tsx:704). The word times are. whisper.cpp's t_dtw marks where the decoder emitted a token, not where the speaker started saying it, and since word.end is the next word's t_dtw, every boundary is late by the same amount — the whole transcript is dragged right by roughly a syllable.

Fix: each boundary moves back to the quietest 10 ms frame within the preceding 150 ms of the same samples whisper was given. Applied in the server adapter, upstream of everything, so trims, caption cues and the transcript pane's playhead highlight are all corrected in one place.

It is self-limiting: on a decaying tail — a word ending a phrase, where whisper is already right — the quietest frame is the reported one, so nothing moves. That is why a constant offset was rejected: it would have fixed the onsets and broken the phrase-ends.

Related issue

Type of change

  • Bug fix

Release impact

  • Patch

Desktop impact

  • Not platform-specific

Measurements

Diagnosed and verified against a real 33.6 s French recording (ggml-small-q8_0) by comparing the stored word times to the signal's own RMS envelope. Measured lag: 80–150 ms, mean 83 ms.

boundary before after audio
trim start (vraiment) 21.94 21.80 ~21.86
trim end (bien) 22.68 22.55 ~22.56
post-pause onset (Alors) 11.96 11.82 11.78
post-pause onset (Je) 18.60 18.45 18.47
post-pause onset (Voilà) 25.04 24.89 24.85
phrase-end (profiter.) 11.20 11.20 (unchanged) 11.23
phrase-end (là.) 23.04 23.04 (unchanged) 23.03

The two phrase-ends are the control: whisper already had them right, and the correction leaves them alone.

Testing

  • npm run test1130/1130 pass (6 new in electron/stt/snapWordBoundaries.test.ts)
  • tsc --noEmit clean, biome check clean, scripts/check-docs.mjs OK
  • Replayed the shipped function over the real project document + the recording's decoded PCM to produce the table above

One real bug surfaced from the existing whisperServer tests while writing this: clamping an out-of-range boundary into the envelope dragged it onto the end of the buffer. A boundary past the decoded audio now stays untouched — there is nothing to measure it against.

Notes for the reviewer

  • Existing transcripts keep their old times. Re-run transcription on a project to pick the correction up; there is no migration (it would mean re-decoding every asset's audio). Say the word if that is wanted.
  • Phrase segments — the fallback whisper only returns when it reports no words at all — are deliberately left alone.
  • LOOKBACK_SEC in snapWordBoundaries.ts is the calibration knob, sized to the measured lag. Widen it and boundaries inside continuous speech start snapping onto the previous syllable's trough.
  • Not exercised in the running app: this is main-process code and needs a real transcription pass to hit. Happy to launch it and transcribe the same recording end to end if you want that before merge.
  • Second-order and deliberately untouched: the preview's trim skip fires ~50 ms early (locateSourcePosition's epsilon, virtual-preview.ts:144) — negligible next to the 150 ms just corrected, and touching it risks the clip-chaining logic.

…ript trims cut where the words are

Deleting words in the transcript editor produced a trim whose edges were
audibly wrong every time: the attack of the first removed word stayed in, and
the cut bit into the following kept word.

The editor is not at fault - it turns a selection into exactly
`[firstWord.startSec, lastWord.endSec]`. The word times are. whisper.cpp's
`t_dtw` marks where the decoder *emitted* a token, not where the speaker
started saying it, and since `word.end` is the next word's `t_dtw`, every
boundary in the transcript is late by the same amount. The whole thing is
dragged right by roughly a syllable.

Measured on a real recording (33.6 s of French, `ggml-small-q8_0`) by
comparing the stored word times against the signal's own RMS envelope: the
lag is 80-150 ms, mean 83 ms. Two independent post-pause onsets confirm it
- "Alors" reported at 11.96 while the audio resumes at 11.78, "Voilà" at
25.04 against 24.85 - and phrase-*ends* are already correct (11.20 against
11.23), so a constant offset would have traded one artifact for another.

So measure instead of guessing: each boundary moves back to the quietest
10 ms frame within the preceding 150 ms of the same samples whisper was
given. That is self-limiting - on a decaying tail the quietest frame IS the
reported one, so the boundaries whisper already got right do not move. The
correction is applied in the server adapter, upstream of everything, so
trims, caption cues and the transcript pane's playhead highlight are all
fixed in one place.

On the trim that prompted the report ("vraiment il est bien"), 21.94-22.68
becomes 21.80-22.55; the audio has the phrase at 21.86-22.56. The post-pause
onsets land at 11.82 and 24.89 against 11.78 and 24.85, and the two
phrase-ends stay put.

One real bug came out of the existing whisperServer tests while writing
this: clamping an out-of-range boundary into the envelope dragged it onto
the end of the buffer. A boundary past the decoded audio now stays untouched
- there is nothing to measure it against.

Existing transcripts keep their old times; re-run transcription on a project
to pick the correction up. Phrase segments (the fallback whisper only
returns when it reports no words at all) are deliberately left alone.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 30d24995-6751-4abb-a8ea-4fe47ce50018

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/transcript-trim-precision-01625f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot changed the base branch from feat/ai-edition to release/1.8.0 July 27, 2026 10:27
@EtienneLescot
EtienneLescot merged commit 1dc867a into release/1.8.0 Jul 27, 2026
1 check passed
@EtienneLescot
EtienneLescot deleted the claude/transcript-trim-precision-01625f branch July 27, 2026 10:27
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.

1 participant