""" Batch audio transcription helper using OpenAI Whisper. Usage (from project root): python -m src.transcribe --audio-dir audio --output data/raw/transcripts.csv The script: - loads a Whisper model (default: small, CPU if CUDA is unavailable), - transcribes all audio files in the given folder, - appends results to a CSV (creates it with a header if missing), - skips files already present in the CSV unless --overwrite is set. """ from __future__ import annotations import argparse import csv import datetime as dt from pathlib import Path from typing import Iterable, List, Set def list_audio_files(folder: Path) -> List[Path]: """Return sorted audio files with common extensions.""" exts = {".wav", ".mp3", ".m4a", ".flac", ".ogg"} return sorted([p for p in folder.glob("*") if p.suffix.lower() in exts]) def load_seen_files(csv_path: Path) -> Set[str]: """Read existing CSV to avoid duplicate transcriptions.""" if not csv_path.exists(): return set() with csv_path.open(newline="", encoding="utf-8") as f: reader = csv.DictReader(f) return {row["file_name"] for row in reader if "file_name" in row} def write_header_if_needed(csv_path: Path, fieldnames: Iterable[str]) -> None: """Create CSV with header when file is missing or empty.""" if csv_path.exists() and csv_path.stat().st_size > 0: return with csv_path.open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() def choose_device(preferred: str | None) -> str: """Select CUDA if available unless user forces a device.""" if preferred: return preferred try: import torch return "cuda" if torch.cuda.is_available() else "cpu" except Exception: return "cpu" def transcribe_file(model, audio_path: Path, language: str | None) -> dict: """Run Whisper transcription and return a compact record.""" result = model.transcribe( str(audio_path), language=language, fp16=getattr(model, "device", None) and model.device.type == "cuda", ) segments = result.get("segments") or [] duration = segments[-1]["end"] if segments else None return { "file_name": audio_path.name, "text": result.get("text", "").strip(), "language": result.get("language"), "duration_s": duration, "timestamp": dt.datetime.utcnow().isoformat() + "Z", } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Batch transcribe audio with Whisper") parser.add_argument( "--audio-dir", type=Path, default=Path(__file__).resolve().parent.parent / "audio", help="Folder containing audio files", ) parser.add_argument( "--output", type=Path, default=Path(__file__).resolve().parent.parent / "data" / "raw" / "transcripts.csv", help="CSV path to append transcripts", ) parser.add_argument( "--model-size", default="small", help="Whisper model size (tiny, base, small, medium, large, etc.)", ) parser.add_argument( "--device", choices=["cpu", "cuda"], default=None, help="Force device; defaults to CUDA if available else CPU", ) parser.add_argument( "--language", default=None, help="Optional language code hint (e.g., 'en', 'hi'); autodetect if omitted", ) parser.add_argument( "--overwrite", action="store_true", help="Recreate output CSV instead of appending/skipping", ) return parser.parse_args() def main() -> None: args = parse_args() audio_dir: Path = args.audio_dir output_csv: Path = args.output device = choose_device(args.device) audio_files = list_audio_files(audio_dir) if not audio_files: print(f"No audio files found in {audio_dir}") return if args.overwrite and output_csv.exists(): output_csv.unlink() output_csv.parent.mkdir(parents=True, exist_ok=True) fields = ["file_name", "text", "language", "duration_s", "timestamp"] write_header_if_needed(output_csv, fields) seen = set() if args.overwrite else load_seen_files(output_csv) import whisper model = whisper.load_model(args.model_size, device=device) with output_csv.open("a", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) for audio_path in audio_files: if audio_path.name in seen: # Skip already processed files unless --overwrite print(f"Skipping {audio_path.name} (already in CSV)") continue print( f"Transcribing {audio_path.name} with model='{args.model_size}' on {device}..." ) record = transcribe_file(model, audio_path, args.language) writer.writerow(record) f.flush() print(f"Done. Results written to {output_csv}") if __name__ == "__main__": main()