| |
| """ |
| run_batch_pipeline.py β Production batch runner for all 192 WAV files. |
| |
| Processes all .wav files from /root/audiodown through the full Voice AI pipeline: |
| 1. Audio Normalization (FFmpeg EBU R128) |
| 2. VAD Segmentation (Silero VAD on CPU) |
| 3. Speaker Diarization (pyannote β ECAPA-TDNN β simple fallback) |
| 4. Emotion Tagging (Wav2Vec2 on GPU) |
| 5. ASR Transcription (Whisper medium on GPU with cuDNN disabled) |
| 6. Quality Validation (SNR/silence/clipping filters) |
| 7. Structured Export (JSONL + CSV + RTTM + manifest.json) |
| 8. Quality Reporting (plots + JSON summary) |
| |
| Features: |
| - GPU-accelerated Whisper (cuDNN disabled for Conv1d compatibility) |
| - Checkpoint-based idempotency (safe to re-run after crash) |
| - Progress tracking with ETA estimates |
| - Detailed per-file timing logs |
| |
| Usage: |
| cd /workspace/Voice AI-int |
| python3 scripts/run_batch_pipeline.py [--limit N] [--hf-token TOKEN] |
| python3 scripts/run_batch_pipeline.py --limit 5 # Test first 5 files |
| python3 scripts/run_batch_pipeline.py --skip-small # Skip files < 10MB |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import os |
| import sys |
| import time |
| import json |
| from pathlib import Path |
|
|
| |
| REPO_ROOT = Path(__file__).resolve().parent.parent |
| os.chdir(REPO_ROOT) |
| sys.path.insert(0, str(REPO_ROOT)) |
|
|
| |
| AUDIO_DIR = Path("/root/audiodown") |
| CONFIG_PATH = REPO_ROOT / "voice_pipeline" / "configs" / "pipeline_config.yaml" |
| LANGUAGE = "hi" |
| CONTENT_TYPE = "debate_podcast" |
|
|
| |
| import voice_pipeline |
| from voice_pipeline.pipeline import SpeechDataPipeline |
| from voice_pipeline.utils.logger import setup_logging, get_logger |
|
|
| |
|
|
| def parse_args() -> argparse.Namespace: |
| p = argparse.ArgumentParser( |
| description="Batch-process all WAV files through the Voice AI Speech Data Pipeline.", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| p.add_argument("--audio-dir", default=str(AUDIO_DIR), |
| help=f"Directory containing .wav files (default: {AUDIO_DIR})") |
| p.add_argument("--config", default=str(CONFIG_PATH), |
| help="Path to pipeline_config.yaml") |
| p.add_argument("--limit", type=int, default=None, |
| help="Process only the first N files (for testing)") |
| p.add_argument("--hf-token", default=None, |
| help="HuggingFace token for pyannote diarization (or set HF_TOKEN env var)") |
| p.add_argument("--language", default=LANGUAGE, |
| help="ISO-639-1 language code (default: hi)") |
| p.add_argument("--log-level", default="INFO", |
| choices=["DEBUG", "INFO", "WARNING", "ERROR"]) |
| p.add_argument("--skip-asr", action="store_true", |
| help="Skip Whisper transcription (faster processing)") |
| p.add_argument("--skip-small", action="store_true", |
| help="Skip files smaller than 5MB (likely incomplete/corrupt)") |
| p.add_argument("--min-size-mb", type=float, default=0.1, |
| help="Minimum file size in MB to process (default: 0.1 = 100KB)") |
| p.add_argument("--max-size-mb", type=float, default=None, |
| help="Maximum file size in MB to process (default: None)") |
| p.add_argument("--resume", action="store_true", default=True, |
| help="Skip already-completed files (default: True)") |
| p.add_argument("--no-resume", dest="resume", action="store_false", |
| help="Re-process all files regardless of checkpoint") |
| return p.parse_args() |
|
|
|
|
| def get_completed_file_ids(checkpoint_dir: Path) -> set[str]: |
| """Read checkpoints and return set of file_ids that completed all stages.""" |
| |
| val_ckpt = checkpoint_dir / "validation_done.jsonl" |
| done_ids: set[str] = set() |
| if val_ckpt.exists(): |
| for line in val_ckpt.open(): |
| try: |
| rec = json.loads(line) |
| if rec.get("status") == "done": |
| done_ids.add(rec["file_id"]) |
| except Exception: |
| pass |
| return done_ids |
|
|
|
|
| def main() -> None: |
| args = parse_args() |
|
|
| |
| if args.hf_token: |
| os.environ["HF_TOKEN"] = args.hf_token |
| print(f"[INFO] HF_TOKEN set from --hf-token argument.") |
| if os.environ.get("HF_TOKEN"): |
| print(f"[INFO] HF_TOKEN found β pyannote diarization will be attempted.") |
| else: |
| print("[WARN] HF_TOKEN not set β ECAPA-TDNN clustering will be used for diarization.") |
|
|
| |
| setup_logging(log_level=args.log_level) |
| log = get_logger("batch_runner") |
|
|
| |
| try: |
| import torch |
| log.info(f"PyTorch: {torch.__version__} | " |
| f"CUDA: {torch.cuda.is_available()} | " |
| f"cuDNN: {torch.backends.cudnn.enabled}") |
| if torch.cuda.is_available(): |
| gpu_props = torch.cuda.get_device_properties(0) |
| log.info(f"GPU: {gpu_props.name} | " |
| f"VRAM: {gpu_props.total_memory / 1e9:.1f} GB") |
| except Exception: |
| pass |
|
|
| |
| audio_dir = Path(args.audio_dir) |
| min_bytes = int(args.min_size_mb * 1_000_000) |
|
|
| wav_files = sorted(audio_dir.glob("*.wav")) |
| wav_files = [f for f in wav_files if f.stat().st_size >= min_bytes] |
|
|
| if args.max_size_mb: |
| max_bytes = int(args.max_size_mb * 1_000_000) |
| wav_files = [f for f in wav_files if f.stat().st_size <= max_bytes] |
|
|
| if args.skip_small: |
| |
| wav_files = [f for f in wav_files if f.stat().st_size >= 5_000_000] |
|
|
| if args.limit: |
| wav_files = wav_files[:args.limit] |
|
|
| total_size_gb = sum(f.stat().st_size for f in wav_files) / 1e9 |
|
|
| log.info( |
| f"Found {len(wav_files)} WAV files " |
| f"({total_size_gb:.1f} GB total) in {audio_dir}" |
| ) |
|
|
| |
| checkpoint_dir = REPO_ROOT / "data" / ".checkpoints" |
| already_done: set[str] = set() |
|
|
| if args.resume: |
| already_done = get_completed_file_ids(checkpoint_dir) |
| if already_done: |
| log.info(f"Resuming: {len(already_done)} files already completed β will skip.") |
|
|
| |
| log.info(f"Initializing Voice AI Speech Pipeline from: {args.config}") |
| pipeline = SpeechDataPipeline(config_path=args.config) |
| log.info("Pipeline ready.") |
|
|
| |
| log.info("=" * 70) |
| log.info(f"STARTING BATCH PROCESSING: {len(wav_files)} files") |
| log.info(f"Language: {args.language} | Content: {CONTENT_TYPE}") |
| log.info("=" * 70) |
|
|
| start_total = time.time() |
| succeeded = 0 |
| failed = 0 |
| skipped = 0 |
| file_timings: list[dict] = [] |
|
|
| for i, wav_path in enumerate(wav_files): |
| from voice_pipeline.utils.file_utils import get_file_hash |
| file_hash = get_file_hash(wav_path, algo="md5") |
| file_id = file_hash[:16] |
|
|
| if args.resume and file_id in already_done: |
| log.info(f"[{i+1}/{len(wav_files)}] {wav_path.name} β Skipping (already completed)") |
| skipped += 1 |
| continue |
|
|
| file_start = time.time() |
| file_label = f"[{i+1}/{len(wav_files)}] {wav_path.name}" |
| file_size_mb = wav_path.stat().st_size / 1e6 |
|
|
| |
| est_duration_min = file_size_mb / 1.92 |
|
|
| log.info( |
| f"\n{file_label} " |
| f"({file_size_mb:.1f}MB ~{est_duration_min:.0f}min)" |
| ) |
|
|
| try: |
| export_dir = pipeline.run_on_file( |
| file_path = wav_path, |
| language = args.language, |
| content_type = CONTENT_TYPE, |
| ) |
| elapsed = time.time() - file_start |
| files_done = i + 1 - skipped |
| elapsed_total = time.time() - start_total |
| avg_per_file = elapsed_total / max(files_done, 1) |
| remaining = avg_per_file * (len(wav_files) - i - 1) |
| eta_mins = remaining / 60 |
|
|
| log.info( |
| f" β Done in {elapsed:.1f}s " |
| f"(RTF={elapsed/max(est_duration_min*60,1):.2f}x) | " |
| f"ETA: {eta_mins:.0f}min" |
| ) |
| succeeded += 1 |
| file_timings.append({ |
| "file": wav_path.name, |
| "size_mb": round(file_size_mb, 1), |
| "elapsed_s": round(elapsed, 1), |
| "status": "ok" |
| }) |
|
|
| except Exception as e: |
| elapsed = time.time() - file_start |
| log.error(f" β Failed after {elapsed:.1f}s: {e}", exc_info=True) |
| failed += 1 |
| file_timings.append({ |
| "file": wav_path.name, |
| "size_mb": round(file_size_mb, 1), |
| "elapsed_s": round(elapsed, 1), |
| "status": "failed", |
| "error": str(e)[:200] |
| }) |
|
|
| |
| total_elapsed = time.time() - start_total |
| log.info("\n" + "=" * 70) |
| log.info("BATCH PROCESSING COMPLETE") |
| log.info("=" * 70) |
| log.info(f" Total files processed: {len(wav_files)}") |
| log.info(f" Succeeded: {succeeded}") |
| log.info(f" Failed: {failed}") |
| log.info(f" Skipped (checkpointed):{skipped}") |
| log.info(f" Total wall time: {total_elapsed/60:.1f} minutes") |
| log.info(f" Avg per file: {total_elapsed/max(len(wav_files),1):.1f} seconds") |
| log.info(f" Exports: {REPO_ROOT}/data/exports/") |
| log.info(f" Reports: {REPO_ROOT}/data/reports/") |
|
|
| |
| summary_path = REPO_ROOT / "data" / "reports" / "batch_run_summary.json" |
| summary_path.parent.mkdir(parents=True, exist_ok=True) |
| with open(summary_path, "w") as f: |
| json.dump({ |
| "total_files": len(wav_files), |
| "succeeded": succeeded, |
| "failed": failed, |
| "skipped": skipped, |
| "total_elapsed_minutes": round(total_elapsed / 60, 2), |
| "avg_per_file_seconds": round(total_elapsed / max(len(wav_files), 1), 1), |
| "file_timings": file_timings, |
| }, f, indent=2) |
| log.info(f" Batch summary: {summary_path}") |
| log.info("=" * 70) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|