Skip to content

Commit d3b0729

Browse files
author
Zenflow
committed
Fix MFA alignment script - use tempfile.TemporaryDirectory() and 16kHz conversion
- Fixed run_mfa_on_10_episodes.py to use working approach from add_word_timestamps_mfa_simple.py - Use tempfile.TemporaryDirectory() for absolute paths (MFA requires this) - Convert audio to 16kHz mono with librosa before MFA - Use correct MFA command order: corpus_dir, dict, model, output_dir - Add --clean, --overwrite, --beam 100, --retry_beam 400 flags - Parse TextGrid output to extract word timestamps - Add test_mfa_episode1.py for debugging MFA issues - Script now successfully processes episodes 1-10
1 parent 6264379 commit d3b0729

2 files changed

Lines changed: 271 additions & 132 deletions

File tree

scripts/run_mfa_on_10_episodes.py

Lines changed: 189 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,229 @@
11
#!/usr/bin/env python3
22
"""
3-
Run MFA alignment on 10 cleaned podcast episodes
4-
Processes one episode at a time to avoid memory issues
3+
Run MFA alignment on 10 cleaned podcast episodes (1-10).
4+
Uses V3-cleaned audio from /Volumes/eHDD/moshi-rag-data/datasets/podcast_clean/
5+
Based on the working add_word_timestamps_mfa_simple.py approach.
56
"""
67

7-
import json
88
import os
9-
import glob
9+
import sys
10+
import json
1011
import subprocess
11-
import shutil
12+
import tempfile
1213
import librosa
1314
import soundfile as sf
14-
import sys
1515
from pathlib import Path
1616

17+
# Unbuffered output
18+
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
19+
1720
# Paths
18-
CLEAN_DIR = "/Volumes/eHDD/moshi-rag-data/datasets/podcast_clean"
19-
OUTPUT_DIR = os.path.join(CLEAN_DIR, "mfa_output")
21+
CLEANED_DIR = "/Volumes/eHDD/moshi-rag-data/datasets/podcast_clean"
22+
TRANSCRIPT_DIR = "/Volumes/eHDD/moshi-rag-data/datasets/Gemischtes.Hack.Podcast/transcripts"
23+
OUTPUT_DIR = "/Volumes/eHDD/moshi-rag-data/datasets/podcast_mfa_aligned"
2024

21-
def process_episode(episode_num, audio_path, metadata_path):
22-
"""Process a single episode through MFA"""
23-
print(f"\n{'='*70}")
24-
print(f"Processing Episode {episode_num}")
25-
print(f"{'='*70}")
26-
27-
# Load metadata to get transcript path
28-
with open(metadata_path, 'r') as f:
29-
metadata = json.load(f)
30-
31-
transcript_path = metadata.get('transcript_path')
32-
if not transcript_path or not os.path.exists(transcript_path):
33-
print(f" ✗ No transcript found")
25+
# MFA models
26+
MFA_DICT = "german_mfa"
27+
MFA_MODEL = "german_mfa"
28+
29+
30+
def parse_textgrid(textgrid_path):
31+
"""Parse MFA TextGrid output to extract word timings."""
32+
words = []
33+
try:
34+
with open(textgrid_path, 'r', encoding='utf-8') as f:
35+
lines = f.readlines()
36+
37+
in_words_tier = False
38+
i = 0
39+
while i < len(lines):
40+
line = lines[i].strip()
41+
42+
if 'name = "words"' in line:
43+
in_words_tier = True
44+
elif in_words_tier and line.startswith('xmin ='):
45+
start = float(line.split('=')[1].strip())
46+
i += 1
47+
end = float(lines[i].strip().split('=')[1].strip())
48+
i += 1
49+
text = lines[i].strip().split('=')[1].strip().strip('"')
50+
if text and text != '':
51+
words.append({
52+
'word': text,
53+
'start': start,
54+
'end': end
55+
})
56+
i += 1
57+
58+
return words
59+
except Exception as e:
60+
print(f" Error parsing TextGrid: {e}", flush=True)
61+
return []
62+
63+
64+
def process_episode(episode_num):
65+
"""Process a single episode with MFA alignment."""
66+
print(f"\n{'='*70}", flush=True)
67+
print(f"Processing Episode {episode_num:03d}", flush=True)
68+
print(f"{'='*70}", flush=True)
69+
70+
# Find cleaned audio file
71+
audio_pattern = f"episode_{episode_num:03d}_*_cleaned.wav"
72+
audio_files = list(Path(CLEANED_DIR).glob(audio_pattern))
73+
74+
if not audio_files:
75+
print(f" ERROR: No cleaned audio found for episode {episode_num}", flush=True)
3476
return False
3577

36-
# Load transcript
37-
print(f" Loading transcript...")
38-
with open(transcript_path, 'r') as f:
39-
transcript_data = json.load(f)
40-
41-
segments = transcript_data['segments']
42-
text_content = " ".join([seg['text'] for seg in segments])
43-
print(f" Segments: {len(segments)}, Words: ~{len(text_content.split())}")
44-
45-
# Create temporary MFA corpus directory for this episode
46-
corpus_dir = f"/tmp/mfa_ep{episode_num:03d}_corpus"
47-
output_dir = f"/tmp/mfa_ep{episode_num:03d}_output"
78+
audio_path = str(audio_files[0])
79+
print(f" Audio: {os.path.basename(audio_path)}", flush=True)
4880

49-
os.makedirs(corpus_dir, exist_ok=True)
50-
os.makedirs(output_dir, exist_ok=True)
81+
# Find transcript
82+
transcript_files = list(Path(TRANSCRIPT_DIR).glob(f"episode_{episode_num:03d}_*.json"))
83+
if not transcript_files:
84+
print(f" ERROR: No transcript found for episode {episode_num}", flush=True)
85+
return False
5186

52-
# Copy audio file directly (MFA will handle conversion)
53-
print(f" Copying audio file...")
54-
mfa_audio_path = os.path.join(corpus_dir, f"episode_{episode_num:03d}.wav")
55-
shutil.copy(audio_path, mfa_audio_path)
56-
print(f" Audio copied")
87+
transcript_path = str(transcript_files[0])
88+
print(f" Transcript: {os.path.basename(transcript_path)}", flush=True)
5789

58-
# Create text file
59-
mfa_text_path = os.path.join(corpus_dir, f"episode_{episode_num:03d}.txt")
60-
with open(mfa_text_path, 'w', encoding='utf-8') as f:
61-
f.write(text_content)
90+
# Load transcript
91+
with open(transcript_path, 'r', encoding='utf-8') as f:
92+
transcript = json.load(f)
6293

63-
# Run MFA alignment
64-
print(f" Running MFA alignment (this may take 5-10 minutes)...")
94+
segments = transcript.get('segments', [])
95+
full_text = ' '.join([seg.get('text', '').strip() for seg in segments if seg.get('text', '').strip()])
6596

66-
mfa_cmd = [
67-
"mfa", "align",
68-
"--clean",
69-
"--single_speaker",
70-
"--beam", "100",
71-
"--retry_beam", "400",
72-
"--output_format", "json",
73-
corpus_dir,
74-
"german_mfa",
75-
"german_mfa",
76-
output_dir
77-
]
97+
print(f" Segments: {len(segments)}", flush=True)
98+
print(f" Text: {len(full_text)} chars, {len(full_text.split())} words", flush=True)
7899

79-
try:
80-
result = subprocess.run(mfa_cmd, capture_output=True, text=True, timeout=1800)
81-
82-
if result.returncode != 0:
83-
print(f" ✗ MFA failed!")
84-
print(f" Error: {result.stderr[:200]}")
85-
return False
86-
87-
# Find output JSON
88-
json_files = list(Path(output_dir).glob("**/*.json"))
89-
if not json_files:
90-
print(f" ✗ No JSON output found")
91-
return False
100+
# Use temporary directory for MFA (this creates absolute paths in /tmp)
101+
with tempfile.TemporaryDirectory() as temp_dir:
102+
# Create corpus directory
103+
corpus_dir = os.path.join(temp_dir, "corpus")
104+
os.makedirs(corpus_dir, exist_ok=True)
92105

93-
# Load and save to final location
94-
with open(json_files[0], 'r') as f:
95-
mfa_result = json.load(f)
106+
# Convert audio to 16kHz mono for MFA
107+
print(f" Converting audio to 16kHz mono...", flush=True)
108+
audio, sr = librosa.load(audio_path, sr=16000, mono=True)
109+
wav_path = os.path.join(corpus_dir, "podcast.wav")
110+
sf.write(wav_path, audio, 16000)
111+
print(f" Audio duration: {len(audio)/16000:.1f}s", flush=True)
96112

97-
# Count words
98-
word_count = 0
99-
if 'tiers' in mfa_result:
100-
tiers = mfa_result['tiers']
101-
if isinstance(tiers, dict) and 'words' in tiers:
102-
word_count = len(tiers['words'].get('entries', []))
103-
elif isinstance(tiers, list):
104-
for tier in tiers:
105-
if tier.get('name') == 'words':
106-
word_count = len(tier.get('entries', []))
113+
# Create text file
114+
txt_path = os.path.join(corpus_dir, "podcast.txt")
115+
with open(txt_path, 'w', encoding='utf-8') as f:
116+
f.write(full_text)
107117

108-
# Save to output directory
109-
os.makedirs(OUTPUT_DIR, exist_ok=True)
110-
output_json = os.path.join(OUTPUT_DIR, f"episode_{episode_num:03d}_mfa.json")
111-
with open(output_json, 'w', encoding='utf-8') as f:
112-
json.dump(mfa_result, f, indent=2, ensure_ascii=False)
118+
# Run MFA alignment
119+
print(f" Running MFA alignment (may take 5-10 minutes)...", flush=True)
113120

114-
print(f" ✓ Success! Aligned {word_count} words")
115-
print(f" Saved to: {output_json}")
121+
output_dir = os.path.join(temp_dir, "output")
122+
os.makedirs(output_dir, exist_ok=True)
116123

117-
# Cleanup temp directories
118-
shutil.rmtree(corpus_dir, ignore_errors=True)
119-
shutil.rmtree(output_dir, ignore_errors=True)
124+
mfa_cmd = [
125+
"mfa", "align",
126+
corpus_dir,
127+
MFA_DICT,
128+
MFA_MODEL,
129+
output_dir,
130+
"--clean",
131+
"--overwrite",
132+
"--beam", "100",
133+
"--retry_beam", "400"
134+
]
120135

121-
return True
136+
print(f" Command: mfa align {corpus_dir} {MFA_DICT} {MFA_MODEL} {output_dir} ...", flush=True)
122137

123-
except subprocess.TimeoutExpired:
124-
print(f" ✗ MFA timed out (>30 minutes)")
125-
return False
126-
except Exception as e:
127-
print(f" ✗ Error: {e}")
128-
return False
138+
try:
139+
result = subprocess.run(mfa_cmd, capture_output=True, text=True, timeout=1800)
140+
141+
if result.returncode != 0:
142+
print(f" ERROR: MFA failed with return code {result.returncode}", flush=True)
143+
print(f" stderr: {result.stderr[:500]}", flush=True)
144+
return False
145+
146+
print(f" MFA alignment complete", flush=True)
147+
148+
# Check for TextGrid output
149+
textgrid_path = os.path.join(output_dir, "podcast.TextGrid")
150+
if not os.path.exists(textgrid_path):
151+
print(f" ERROR: TextGrid not found", flush=True)
152+
return False
153+
154+
# Parse TextGrid
155+
words = parse_textgrid(textgrid_path)
156+
print(f" Words aligned: {len(words)}", flush=True)
157+
158+
if not words:
159+
print(f" ERROR: No words extracted from TextGrid", flush=True)
160+
return False
161+
162+
# Save results
163+
os.makedirs(OUTPUT_DIR, exist_ok=True)
164+
output_path = os.path.join(OUTPUT_DIR, f"episode_{episode_num:03d}_mfa.json")
165+
166+
result_data = {
167+
'episode': episode_num,
168+
'audio_path': audio_path,
169+
'transcript_path': transcript_path,
170+
'word_count': len(words),
171+
'segment_count': len(segments),
172+
'words': words
173+
}
174+
175+
with open(output_path, 'w', encoding='utf-8') as f:
176+
json.dump(result_data, f, indent=2, ensure_ascii=False)
177+
178+
print(f" SUCCESS: Saved to {os.path.basename(output_path)}", flush=True)
179+
return True
180+
181+
except subprocess.TimeoutExpired:
182+
print(f" ERROR: MFA timed out (>30 minutes)", flush=True)
183+
return False
184+
except Exception as e:
185+
print(f" ERROR: {e}", flush=True)
186+
return False
187+
129188

130189
def main():
131-
print("="*70)
132-
print("MFA Alignment for 10 Cleaned Episodes")
133-
print("="*70)
134-
print(f"Output directory: {OUTPUT_DIR}")
135-
print()
136-
137-
# Find all cleaned audio files
138-
audio_files = sorted(glob.glob(os.path.join(CLEAN_DIR, "episode_*_cleaned.wav")))
139-
print(f"Found {len(audio_files)} cleaned audio files")
190+
"""Main entry point."""
191+
print("="*70, flush=True)
192+
print("MFA Alignment for 10 Cleaned Episodes", flush=True)
193+
print("="*70, flush=True)
194+
print(f"Input: {CLEANED_DIR}", flush=True)
195+
print(f"Output: {OUTPUT_DIR}", flush=True)
196+
print(flush=True)
197+
198+
# Check MFA installation
199+
try:
200+
result = subprocess.run(["mfa", "version"], capture_output=True, text=True)
201+
print(f"MFA version: {result.stdout.strip()}", flush=True)
202+
except Exception as e:
203+
print(f"ERROR: MFA not installed: {e}", flush=True)
204+
return 1
140205

141-
# Process each episode
206+
# Process episodes 1-10
142207
results = []
143-
for i, audio_path in enumerate(audio_files, 1):
144-
basename = os.path.basename(audio_path)
145-
episode_num = int(basename.split('_')[1])
146-
metadata_path = audio_path.replace('_cleaned.wav', '_metadata.json')
147-
148-
if not os.path.exists(metadata_path):
149-
print(f"\nEpisode {episode_num}: No metadata found, skipping")
150-
results.append((episode_num, False))
151-
continue
152-
153-
success = process_episode(episode_num, audio_path, metadata_path)
208+
for episode_num in range(1, 11):
209+
success = process_episode(episode_num)
154210
results.append((episode_num, success))
155-
156-
print(f"\nProgress: {i}/{len(audio_files)} episodes processed")
157211

158212
# Summary
159-
print("\n" + "="*70)
160-
print("Summary")
161-
print("="*70)
213+
print("\n" + "="*70, flush=True)
214+
print("Summary", flush=True)
215+
print("="*70, flush=True)
162216
successful = sum(1 for _, success in results if success)
163-
print(f"Successful: {successful}/{len(results)}")
164-
print(f"Failed: {len(results) - successful}/{len(results)}")
217+
print(f"Successful: {successful}/10", flush=True)
218+
print(f"Failed: {10 - successful}/10", flush=True)
165219

166220
if successful > 0:
167-
print(f"\nMFA output files saved to: {OUTPUT_DIR}")
221+
print(f"\nMFA output files saved to: {OUTPUT_DIR}", flush=True)
168222

169-
return 0 if successful == len(results) else 1
223+
return 0 if successful == 10 else 1
224+
170225

171226
if __name__ == "__main__":
172-
sys.exit(main())
227+
sys.exit(main())
228+
229+
# Made with Bob

0 commit comments

Comments
 (0)