audio: CrisperWhisper long-form overlap windowing (no seam drops)

Contiguous windows dropped words at each ~28s seam (Whisper's weak edge context +
words straddling the boundary). Now use overlapping windows (29s wide, 24s hop =
5s overlap), advancing by the hop and transcribing each full window, then
de-duplicate the overlap by keeping only words that start at/after the last kept
word's end, and rebuild the transcript from the de-duped words. Verified on an
~11min clip: 0 gaps >2s across all seams, continuous word timestamps to 673s,
recovered the previously-dropped seam words.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
parent b0483e8c
...@@ -71,8 +71,12 @@ class _Engine: ...@@ -71,8 +71,12 @@ class _Engine:
torch_dtype=dtype, device=dev) torch_dtype=dtype, device=dev)
# Whisper's receptive field is 30s; stay a touch under so each window is a # Whisper's receptive field is 30s; stay a touch under so each window is a
# single, non-chunked pass. # single, non-chunked pass. Advance by HOP_S (< WINDOW_S) so consecutive
WINDOW_S = 28.0 # windows OVERLAP — Whisper drops/mangles words at a window's edges, so each
# word is re-seen with full context in the next window and de-duplicated by
# timestamp. (WINDOW_S - HOP_S) is the overlap.
WINDOW_S = 29.0
HOP_S = 24.0
def _one(self, arr, sr, gen, offset): def _one(self, arr, sr, gen, offset):
"""Transcribe one ≤30s window; return (text, [word dicts]) with timestamps """Transcribe one ≤30s window; return (text, [word dicts]) with timestamps
...@@ -97,22 +101,30 @@ class _Engine: ...@@ -97,22 +101,30 @@ class _Engine:
if getattr(data, "ndim", 1) > 1: if getattr(data, "ndim", 1) > 1:
data = data.mean(axis=1) data = data.mean(axis=1)
dur = len(data) / sr dur = len(data) / sr
texts, words = [], [] words = []
if dur <= 30.0: if dur <= self.WINDOW_S:
t, w = self._one(data, sr, gen, 0.0) _, w = self._one(data, sr, gen, 0.0)
texts.append(t); words.extend(w) words.extend(w)
else: else:
# Manual long-form windowing (avoids the pipeline's buggy stitching). # Overlapping windows (WINDOW_S wide, advancing by HOP_S) so no audio
# is lost at the seams. De-dup the overlap region by keeping only words
# that start at/after the last kept word's end.
last_end = -1.0
pos = 0.0 pos = 0.0
while pos < dur: while pos < dur:
a = int(pos * sr) a = int(pos * sr)
b = int(min(pos + self.WINDOW_S, dur) * sr) b = int(min(pos + self.WINDOW_S, dur) * sr)
t, w = self._one(data[a:b], sr, gen, pos) _, w = self._one(data[a:b], sr, gen, pos)
if t: for x in w:
texts.append(t) if x["start"] >= last_end - 0.05: # skip words already captured
words.extend(w) words.append(x)
pos += self.WINDOW_S last_end = max(last_end, x["end"])
text = " ".join(x for x in texts if x).strip() if b >= len(data):
break
pos += self.HOP_S
# Rebuild text from the de-duplicated words (word-first model), so the
# overlap isn't double-counted in the transcript either.
text = " ".join(x["word"] for x in words).strip()
segments = [] segments = []
if words: if words:
segments = [{"start": words[0]["start"], "end": words[-1]["end"], segments = [{"start": words[0]["start"], "end": words[-1]["end"],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment