Skip to content

Commit 74d2b53

Browse files
author
Zenflow
committed
bootstrap: LCS first-segment alignment instead of 4-gram scan
Replace the exact 4-gram scan with a true LCS search over the first 180s of whisper output for each of the first 40 transcript segments. Pick the candidate with the earliest raw timestamp (first content after intro jingle). This is strictly better: - Tolerates whisper dropping the first 1-2 words of a segment (common at intro/ad boundaries) — LCS scores remaining words correctly. - Immune to accidental 4-gram repeats mid-episode that caused the +2034s false bootstrap in the previous session. - Exact 4-gram scan kept as fallback for degenerate cases. Result on ep152: bootstrap=+36.17s (was +34.21s with bounded 4-gram), single recalibration at known ad break, 88.6% words assigned.
1 parent 37b564f commit 74d2b53

1 file changed

Lines changed: 60 additions & 14 deletions

File tree

scripts/podcast_to_moshi_dataset.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,67 @@ def _bootstrap_offset(whisper_words: list, transcript_segs: list,
259259
max_segs: int = 40,
260260
max_raw_t: float = 180.0) -> float | None:
261261
"""
262-
Estimate the initial raw_time − transcript_time offset by finding the
263-
first 4-word exact run shared between the transcript (first max_segs
264-
segments) and the whisper output (first max_raw_t seconds only).
265-
266-
Constraining to the first max_raw_t seconds of whisper prevents a rare
267-
false match deep in the audio from producing a wildly wrong initial offset.
262+
Estimate the initial raw_time − transcript_time offset.
263+
264+
Strategy (primary): for each of the first max_segs transcript segments
265+
with ≥4 words, run a true LCS search within the first max_raw_t seconds
266+
of whisper output. The first segment that scores above threshold gives
267+
the offset as: raw_start_of_match − transcript_seg_start.
268+
269+
This is better than a 4-gram exact scan because:
270+
- It tolerates whisper dropping the first 1-2 words of a segment (common
271+
at intro boundaries) — true LCS scores the remaining words correctly.
272+
- It scores the best-matching position in the window rather than the
273+
first exact 4-gram, so it's robust to accidental 4-gram repeats
274+
elsewhere in the audio.
275+
- Bounding to max_raw_t ensures we can never get a false offset from a
276+
repeated phrase deep in the episode.
277+
278+
Fallback: if LCS finds nothing, try an exact 4-gram scan in the same
279+
window (handles cases where all early segments are very short and LCS
280+
thresholds are never met).
268281
269282
Returns offset (float) or None if nothing found.
270283
"""
271-
w_norm = [((norm_words(w["word"]) or [""])[0]) for w in whisper_words]
272-
w_starts = [w["start"] for w in whisper_words]
284+
w_starts_all = [w["start"] for w in whisper_words]
273285

274-
# Only search within the first max_raw_t seconds of whisper output
275-
wi_limit = bisect.bisect_right(w_starts, max_raw_t)
286+
# Window: first max_raw_t seconds only
287+
hi_idx = bisect.bisect_right(w_starts_all, max_raw_t)
288+
if hi_idx == 0:
289+
return None
276290

291+
w_window = whisper_words[:hi_idx]
292+
w_starts = w_starts_all[:hi_idx]
293+
294+
def _min_score(n: int) -> float:
295+
if n <= 4: return 0.75
296+
if n <= 6: return 0.67
297+
if n <= 10: return 0.60
298+
return 0.55
299+
300+
# ── Primary: LCS search across all early segments, pick earliest raw hit ─
301+
# We collect all candidates and return the one with the smallest raw_time,
302+
# because intro jingles/ads always precede content — the first content
303+
# segment to appear in whisper output anchors the offset most accurately.
304+
best_raw_t = float("inf")
305+
best_offset = None
306+
mid = w_starts[-1] / 2.0
307+
for seg in transcript_segs[:max_segs]:
308+
tn = norm_words(seg["text"])
309+
n = len(tn)
310+
t_start = float(seg["start"])
311+
if n < 4:
312+
continue
313+
abs_s, abs_e, score = search_segment(
314+
tn, w_window, w_starts, 0, mid, mid + 1.0, _min_score(n))
315+
if abs_s is not None and w_starts[abs_s] < best_raw_t:
316+
best_raw_t = w_starts[abs_s]
317+
best_offset = w_starts[abs_s] - t_start
318+
if best_offset is not None:
319+
return best_offset
320+
321+
# ── Fallback: exact 4-gram scan ───────────────────────────────────────────
322+
w_norm = [((norm_words(w["word"]) or [""])[0]) for w in w_window]
277323
for seg in transcript_segs[:max_segs]:
278324
tn = norm_words(seg["text"])
279325
t_start = float(seg["start"])
@@ -282,12 +328,12 @@ def _bootstrap_offset(whisper_words: list, transcript_segs: list,
282328
continue
283329
for gi in range(len(tn) - 3):
284330
gram = tn[gi:gi + 4]
285-
for wi in range(min(wi_limit, len(w_norm) - 3)):
331+
for wi in range(len(w_norm) - 3):
286332
if w_norm[wi:wi + 4] == gram:
287333
word_frac = gi / len(tn)
288334
approx_gram_t = t_start + word_frac * seg_dur
289-
offset = w_starts[wi] - approx_gram_t
290-
return offset
335+
return w_starts[wi] - approx_gram_t
336+
291337
return None
292338

293339

@@ -331,7 +377,7 @@ def min_score(n: int) -> float:
331377
# ── Bootstrap offset via 4-gram exact scan ────────────────────────────────
332378
offset = _bootstrap_offset(whisper_words, transcript_segs)
333379
if offset is not None:
334-
print(f" step2: bootstrap offset={offset:+.2f}s (4-gram scan)", flush=True)
380+
print(f" step2: bootstrap offset={offset:+.2f}s", flush=True)
335381
else:
336382
offset = 0.0
337383
print(f" step2: bootstrap failed, starting with offset=0.0", flush=True)

0 commit comments

Comments
 (0)