Spaces:
Paused
Paused
| """Forced-alignment fallback for word timings. | |
| When a TTS provider returns no word timings, we try faster-whisper (if | |
| installed) to recover them from the rendered audio. faster-whisper is an | |
| OPTIONAL dependency — when missing or failing, return None and the render | |
| stage degrades to static (per-scene) captions instead of karaoke. | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional | |
| from core.contracts import WordTiming | |
| from core.utils import log | |
| _model = None | |
| _model_failed = False | |
| def align_words(audio_path: str, text: str) -> Optional[list[WordTiming]]: | |
| """Best-effort word timings for ``audio_path``. None on any failure.""" | |
| global _model, _model_failed | |
| if _model_failed: | |
| return None | |
| try: | |
| if _model is None: | |
| from faster_whisper import WhisperModel # type: ignore | |
| _model = WhisperModel("tiny", device="cpu", compute_type="int8") | |
| segments, _info = _model.transcribe(audio_path, word_timestamps=True) | |
| timings: list[WordTiming] = [] | |
| for seg in segments: | |
| for w in seg.words or []: | |
| timings.append(WordTiming(word=w.word.strip(), start=w.start, end=w.end)) | |
| return timings or None | |
| except ImportError: | |
| log.info("faster-whisper not installed — captions will be static (pip install faster-whisper to enable karaoke alignment)") | |
| _model_failed = True | |
| return None | |
| except Exception as e: | |
| log.warning("Forced alignment failed (%s) — falling back to static captions", e) | |
| return None | |