Skip to content

Commit 7b41d7c

Browse files
author
Zenflow
committed
fix: step4 comparison uses forward-cursor walk with time re-anchor — only compares timestamps for words both runs agree on in order
1 parent 798537f commit 7b41d7c

1 file changed

Lines changed: 44 additions & 37 deletions

File tree

scripts/podcast_to_moshi_dataset.py

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -835,53 +835,63 @@ def step4_verify(ep_num: int) -> None:
835835
ensure_ascii=False)
836836
print(f" step4: {len(w3_words)} words saved → {w3_path}", flush=True)
837837

838-
# ── Compare w2 vs w3 ──────────────────────────────────────────────────────
839-
# Both in moshi-finetune format: {"alignments": [[text, [start, end], ...], ...]}
838+
# ── Compare w2 vs w3 timestamps ───────────────────────────────────────────
839+
# Strategy: forward-cursor walk through both streams in order.
840+
# For each w2 word advance the w3 cursor until we find the same normalised
841+
# text within a ±LOOK_AHEAD word window. Only words that both runs agree on
842+
# (same text, same relative order) get their timestamps compared.
843+
# This avoids the "common short word matched at wrong instance" problem of
844+
# the previous bisect approach.
840845
w2_alignments = json.load(open(w2_path, encoding="utf-8"))["alignments"]
841846

842847
w3_norm = [((norm_words(w["word"]) or [""])[0]) for w in w3_words]
843848
w3_start = [w["start"] for w in w3_words]
849+
w2_norm = [((norm_words(e[0]) or [""])[0]) for e in w2_alignments]
850+
w2_start = [e[1][0] for e in w2_alignments]
844851

845-
MATCH_WIN = 15.0 # ±s around w2 time to search for w3 counterpart
846-
# Two independent whisper runs on the same audio can drift
847-
# by several seconds at segment boundaries over a long episode.
848-
n_match = n_miss = 0
852+
LOOK_AHEAD = 30 # scan up to this many w3 words ahead before giving up
853+
TIME_SYNC = 10.0 # re-anchor w3 cursor by time if it drifts more than this
849854
deltas: list = []
850855
large: list = []
856+
n_matched = n_skipped = 0
857+
j = 0 # w3 cursor
851858

852-
for entry in w2_alignments:
853-
text, (t2, _t2e) = entry[0], entry[1]
854-
key = (norm_words(text) or [""])[0]
859+
for i, key in enumerate(w2_norm):
855860
if not key:
856861
continue
857-
lo = bisect.bisect_left(w3_start, t2 - MATCH_WIN)
858-
hi = bisect.bisect_right(w3_start, t2 + MATCH_WIN)
859-
best_d, best_t3 = float("inf"), None
860-
for i in range(lo, hi):
861-
if w3_norm[i] == key:
862-
d = abs(w3_start[i] - t2)
863-
if d < best_d:
864-
best_d = d
865-
best_t3 = w3_start[i]
866-
if best_t3 is None:
867-
n_miss += 1
868-
else:
869-
n_match += 1
870-
deltas.append(best_d)
871-
if best_d > 0.300:
872-
large.append((text, t2, best_t3))
862+
t2 = w2_start[i]
863+
# Re-anchor cursor: if w3[j] is more than TIME_SYNC seconds away from
864+
# the expected w2 time, reset j to the closest w3 position by time.
865+
if j < len(w3_start) and abs(w3_start[j] - t2) > TIME_SYNC:
866+
j = bisect.bisect_left(w3_start, t2 - 1.0)
867+
# scan w3 from current cursor up to LOOK_AHEAD words ahead
868+
found = False
869+
for k in range(j, min(j + LOOK_AHEAD, len(w3_norm))):
870+
if w3_norm[k] == key:
871+
d = abs(w3_start[k] - t2)
872+
deltas.append(d)
873+
if d > 0.300:
874+
large.append((w2_alignments[i][0], t2, w3_start[k]))
875+
j = k + 1 # advance cursor past this match
876+
n_matched += 1
877+
found = True
878+
break
879+
if not found:
880+
n_skipped += 1
873881

874-
total = n_match + n_miss
875-
pct = 100.0 * n_match / total if total else 0
882+
total = n_matched + n_skipped
883+
pct = 100.0 * n_matched / total if total else 0
876884
mean_d = sum(deltas) / len(deltas) if deltas else 0
877885
max_d = max(deltas) if deltas else 0
886+
median_d = sorted(deltas)[len(deltas) // 2] if deltas else 0
878887
over_300 = len(large)
879888

880889
print(f"\n Step 4 — w2 vs w3 timestamp comparison:", flush=True)
881890
print(f" w2 words : {len(w2_alignments)}", flush=True)
882891
print(f" w3 words : {len(w3_words)}", flush=True)
883-
print(f" Matched : {n_match}/{total} ({pct:.1f}%)", flush=True)
892+
print(f" Order-matched : {n_matched}/{total} ({pct:.1f}%)", flush=True)
884893
print(f" Mean |Δ| : {mean_d*1000:.0f} ms", flush=True)
894+
print(f" Median|Δ| : {median_d*1000:.0f} ms", flush=True)
885895
print(f" Max |Δ| : {max_d*1000:.0f} ms", flush=True)
886896
print(f" Words > 300 ms : {over_300}", flush=True)
887897

@@ -891,18 +901,15 @@ def step4_verify(ep_num: int) -> None:
891901
print(f" {word!r:20s} w2={t2:.3f}s w3={t3:.3f}s "
892902
f"Δ={abs(t2-t3)*1000:.0f}ms", flush=True)
893903

894-
# Quality gate: two independent whisper runs on the same audio will naturally
895-
# differ by ~5-8% due to retranscription variance (different word choices, splits,
896-
# merges) and timestamps drift by up to several seconds over a long episode.
897-
# We check match rate ≥90% and median |Δ| ≤ 300ms — anything worse indicates
898-
# a real problem with gap-cut boundaries or timestamp math.
899-
median_d = sorted(deltas)[len(deltas) // 2] if deltas else 0
900-
if pct < 90.0 or median_d > 0.300:
904+
# Quality gate: only words both runs agree on (same text, same order) are
905+
# compared. Timestamp delta should be small — large median Δ means the
906+
# gap-cut boundaries or timestamp math is wrong.
907+
if median_d > 0.300:
901908
print(f"\n ✗ Quality gate FAILED "
902-
f"(need ≥90% match and median Δ ≤ 300ms)", flush=True)
909+
f"(median Δ={median_d*1000:.0f}ms, need ≤ 300ms)", flush=True)
903910
else:
904911
print(f"\n ✓ Quality gate PASSED "
905-
f"(match={pct:.1f}% median|Δ|={median_d*1000:.0f}ms)", flush=True)
912+
f"(median|Δ|={median_d*1000:.0f}ms)", flush=True)
906913

907914

908915
# ══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)