"""Run Phase 1 on every WAV in a folder, writing per-segment JSON. Bypasses the annotation-tool DB — produces standalone JSON output for one-off tests. Reuses the same `annotate.phase1` modules so the analysis is identical to what the annotation tool produces. Usage: python scripts/run_phase1_on_folder.py SEGMENTS_DIR OUT_DIR LANGUAGE Example: python scripts/run_phase1_on_folder.py \\ data/hindi_emphasis_test/segments \\ data/hindi_emphasis_test/phase1 \\ hi """ from __future__ import annotations import json import logging import sys from pathlib import Path # Make annotate/ importable when running this script from anywhere sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import time from annotate.phase1.config import PIPELINE_VERSION from annotate.phase1.emphasis import score_emphasis from annotate.phase1.emotion import classify_emotion, offload_emotion_model from annotate.phase1.prosody import extract_per_word_prosody from annotate.phase1.transcribe import transcribe log = logging.getLogger(__name__) def process_one(wav_path: Path, language: str) -> dict: """Run the full Phase 1 pipeline on one WAV; return a JSON-serializable dict.""" t0 = time.time() # 1. Transcribe (WhisperX + forced alignment). asr = transcribe(wav_path, language=language) # 2. Prosody per word (parselmouth F0 + energy). windows = [(w.start, w.end) for w in asr.words] prosody = extract_per_word_prosody(wav_path, windows) f0_peaks = [p[0] for p in prosody] energy_peaks = [p[1] for p in prosody] # 3. Emphasis flags. duration = windows[-1][1] if windows else 0.0 flags = score_emphasis(windows, f0_peaks, energy_peaks, duration) # 4. Emotion (segment-level via emotion2vec+). try: emo = classify_emotion(wav_path) emotion_obj = { "label": emo.label, "confidence": emo.confidence, "scores": emo.scores, } except Exception as e: emotion_obj = {"error": f"{type(e).__name__}: {e}"} elapsed = round(time.time() - t0, 2) return { "segment": wav_path.name, "language": asr.language, "duration_seconds": round(duration, 3), "text": asr.text, "n_words": len(asr.words), "n_emphasized": int(sum(flags)), "words": [ { "idx": i, "text": w.text, "start": round(w.start, 3), "end": round(w.end, 3), "emphasis": bool(flags[i]), "f0_peak": round(f0_peaks[i], 1) if f0_peaks[i] is not None else None, "energy_peak": round(energy_peaks[i], 1) if energy_peaks[i] is not None else None, } for i, w in enumerate(asr.words) ], "emotion": emotion_obj, "phase1_version": PIPELINE_VERSION, "elapsed_seconds": elapsed, } def main(seg_dir: Path, out_dir: Path, language: str) -> None: out_dir.mkdir(parents=True, exist_ok=True) wavs = sorted(p for p in seg_dir.glob("*.wav") if not p.name.startswith("_")) if not wavs: print(f"No WAVs found in {seg_dir}") return print(f"Processing {len(wavs)} segments (language={language})...") for wav in wavs: try: result = process_one(wav, language=language) out_path = out_dir / (wav.stem + ".json") out_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8") print( f" {wav.name}: {result['n_words']:3d} words, " f"{result['n_emphasized']} emphasized, " f"emotion={result['emotion'].get('label', 'err')}, " f"{result['elapsed_seconds']}s" ) except Exception as e: log.exception("phase1 failed for %s", wav) err_path = out_dir / (wav.stem + ".error.json") err_path.write_text(json.dumps({"error": str(e)}, indent=2), encoding="utf-8") print(f" {wav.name}: FAILED -- {e}") # Free emotion model GPU memory at the end try: offload_emotion_model() except Exception: pass print(f"Done. Outputs in {out_dir}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage: run_phase1_on_folder.py SEGMENTS_DIR OUT_DIR LANGUAGE") sys.exit(1) logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s") for n in ("transformers", "pyannote", "pytorch_lightning", "huggingface_hub", "modelscope", "funasr", "root"): logging.getLogger(n).setLevel(logging.ERROR) main(Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3])