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
88import os
9- import glob
9+ import sys
10+ import json
1011import subprocess
11- import shutil
12+ import tempfile
1213import librosa
1314import soundfile as sf
14- import sys
1515from 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
130189def 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"\n Episode { 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"\n Progress: { 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"\n MFA output files saved to: { OUTPUT_DIR } " )
221+ print (f"\n MFA 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
171226if __name__ == "__main__" :
172- sys .exit (main ())
227+ sys .exit (main ())
228+
229+ # Made with Bob
0 commit comments