from __future__ import annotations from pathlib import Path from typing import Any from .models import WordTiming def _clean_word(value: str) -> str: return value.strip() def transcribe_and_align( audio_path: Path, *, device: str = "cuda", compute_type: str = "float16", model_name: str = "large-v3", language: str | None = None, batch_size: int = 8, ) -> tuple[str, list[WordTiming], dict[str, Any]]: """Transcribe actual audio and force-align every recognized word.""" try: import whisperx except ImportError as exc: raise RuntimeError( "WhisperX is not installed. Run the project installer first." ) from exc audio = whisperx.load_audio(str(audio_path)) model = whisperx.load_model( model_name, device, compute_type=compute_type, language=language, ) result = model.transcribe(audio, batch_size=batch_size, language=language) detected_language = result.get("language") or language or "unknown" align_model, metadata = whisperx.load_align_model( language_code=detected_language, device=device, ) aligned = whisperx.align( result["segments"], align_model, metadata, audio, device, return_char_alignments=True, ) word_segments = aligned.get("word_segments") or [] words: list[WordTiming] = [] for raw in word_segments: word = _clean_word(str(raw.get("word", ""))) start = raw.get("start") end = raw.get("end") score = raw.get("score") if not word: continue review = start is None or end is None if review: # Preserve unaligned words for review, but never invent a timestamp. continue words.append(WordTiming( index=len(words), word=word, start=float(start), end=float(end), score=float(score) if score is not None else None, review_required=(score is not None and float(score) < 0.45), )) transcript = " ".join(word.word for word in words) metadata_out = { "language": detected_language, "model": model_name, "alignment_backend": "whisperx", "segments": aligned.get("segments", []), } return transcript, words, metadata_out