Skip to content

Commit 8e56628

Browse files
author
Zenflow
committed
podcast alignment: true LCS, 4-gram bootstrap offset, consecutive-miss recalibration
Step 2 complete rewrite with 5 bug fixes: - Replace greedy anchored lcs_score() with true O(n·m) DP LCS (_true_lcs_len, _find_lcs_span). Old code required t_norm[0] to appear in chunk, scoring 0.0 whenever whisper dropped the first word(s) of a segment. - Replace sliding WIN=n+n//3 sub-window in search_segment() with a single true LCS pass over the full time window. No sliding needed; DP finds best span regardless of where the match starts or ends. - Bootstrap offset via 4-gram exact-run scan (_bootstrap_offset). Previous offset=None bootstrapped by searching [-90,+90]s around t=0, finding only sponsor jingle and never initialising the offset. 4-gram scan reliably detects offset=+33.61s for ep152 from 'goes turning into some'. - Replace forward-anchored wide-window fallback with consecutive-miss recalibration: after N_MISS_AD=8 misses on >=4-word segments, search ±RECAL_WIN=60s around expected position with cursor as floor. Eliminates the offset oscillation (+/-100s per segment) from the old cursor_t+WIDE_WIN strategy. - Exclude short segments (<MIN_WORDS=4) from pending_miss queue. They were accumulating and triggering false recalibrations. Interpolation handles them. - Add back-fill pass: segments that fail both tight and recal windows are queued in pending_miss and resolved backwards from the next successful anchor. Results on ep152: 89.2% words assigned (vs 86.4%), 1636/1800 eligible segments matched, 2 clean gap regions (34s intro + 8s gap), 2 accurate recalibrations at the known StepStone ad break (~t=1400s). Also add untracked experiment/analysis scripts from ep150-159 MFA/whisper work.
1 parent 5df84e4 commit 8e56628

11 files changed

Lines changed: 2257 additions & 215 deletions

scripts/aggregate_mfa_outputs.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Aggregate all MFA segment outputs into a single comprehensive JSON file
4+
"""
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
10+
def aggregate_mfa_segments(mfa_output_dir, transcript_path, output_path):
11+
"""Aggregate all MFA segment JSON files into one comprehensive output"""
12+
13+
print("Loading original transcript for segment timing...")
14+
with open(transcript_path, 'r', encoding='utf-8') as f:
15+
transcript_data = json.load(f)
16+
17+
segments = transcript_data['segments']
18+
max_duration = 300 # 5 minutes
19+
filtered_segments = [seg for seg in segments if seg['start'] < max_duration]
20+
21+
print(f"Found {len(filtered_segments)} segments in first 5 minutes")
22+
23+
# Find all MFA output JSON files
24+
json_files = sorted(Path(mfa_output_dir).glob("seg_*.json"))
25+
print(f"Found {len(json_files)} MFA output files")
26+
27+
# Aggregate all words with adjusted timestamps
28+
all_words = []
29+
segments_processed = 0
30+
31+
for json_file in json_files:
32+
# Extract segment index from filename (e.g., seg_0001.json -> 1)
33+
seg_idx = int(json_file.stem.split('_')[1])
34+
35+
if seg_idx >= len(filtered_segments):
36+
print(f"Warning: Segment index {seg_idx} out of range, skipping")
37+
continue
38+
39+
# Get the original segment timing
40+
original_segment = filtered_segments[seg_idx]
41+
segment_start = original_segment['start']
42+
43+
# Load MFA output for this segment
44+
with open(json_file, 'r', encoding='utf-8') as f:
45+
mfa_data = json.load(f)
46+
47+
# Extract words and adjust timestamps
48+
if 'tiers' in mfa_data:
49+
tiers = mfa_data['tiers']
50+
if isinstance(tiers, dict) and 'words' in tiers:
51+
entries = tiers['words'].get('entries', [])
52+
for entry in entries:
53+
start, end, text = entry
54+
if text: # Skip empty entries
55+
# Adjust timestamps relative to full audio
56+
adjusted_start = segment_start + start
57+
adjusted_end = segment_start + end
58+
all_words.append({
59+
'text': text,
60+
'start': adjusted_start,
61+
'end': adjusted_end,
62+
'duration': end - start,
63+
'segment_idx': seg_idx
64+
})
65+
66+
segments_processed += 1
67+
68+
print(f"\nProcessed {segments_processed} segments")
69+
print(f"Total words extracted: {len(all_words)}")
70+
71+
# Sort by start time
72+
all_words.sort(key=lambda x: x['start'])
73+
74+
# Create output structure
75+
output_data = {
76+
'source': 'MFA alignment',
77+
'audio_duration': max_duration,
78+
'segments_processed': segments_processed,
79+
'total_words': len(all_words),
80+
'words': all_words
81+
}
82+
83+
# Save aggregated output
84+
with open(output_path, 'w', encoding='utf-8') as f:
85+
json.dump(output_data, f, indent=2, ensure_ascii=False)
86+
87+
print(f"\nAggregated output saved to: {output_path}")
88+
89+
# Show first 20 words
90+
print("\nFirst 20 words:")
91+
for i, word in enumerate(all_words[:20]):
92+
print(f" {i+1}. [{word['start']:.3f}s - {word['end']:.3f}s] {word['text']}")
93+
94+
return output_data
95+
96+
def main():
97+
mfa_output_dir = "/tmp/mfa_output_ep150_5min"
98+
transcript_path = "/Volumes/eHDD/moshi-rag-data/datasets/Gemischtes.Hack.Podcast/transcripts_aligned_final/episode_150_gemischtes_hack.json"
99+
output_path = "/Volumes/eHDD/moshi-rag-data/datasets/whisper_cpp_test/episode_150_5min_mfa_aligned_full.json"
100+
101+
print("="*70)
102+
print("Aggregating MFA Segment Outputs")
103+
print("="*70)
104+
print()
105+
106+
result = aggregate_mfa_segments(mfa_output_dir, transcript_path, output_path)
107+
108+
print("\n" + "="*70)
109+
print("Aggregation Complete")
110+
print("="*70)
111+
print(f"\nTotal words: {result['total_words']}")
112+
print(f"Segments processed: {result['segments_processed']}")
113+
print(f"Duration: {result['audio_duration']}s")
114+
115+
if __name__ == "__main__":
116+
main()
117+
118+
# Made with Bob

0 commit comments

Comments
 (0)