"""Speech-to-text. Primary path is Gemma 4's audio modality (one model for the whole pipeline). Gemma caps audio at ~30s, so longer notes fall back to faster-whisper, which handles any length. If the duration can't be probed, we try Gemma and fall back on any error. """ from __future__ import annotations import threading import gemma import llama_backend from config import ( GEMMA_AUDIO_MAX_SEC, WHISPER_BEAM, WHISPER_COMPUTE, WHISPER_DEVICE, WHISPER_LANG, WHISPER_MODEL, ) _whisper = None _lock = threading.Lock() def _duration(audio_path: str) -> float | None: """Best-effort audio length in seconds; None if it can't be determined.""" try: import librosa return float(librosa.get_duration(path=audio_path)) except Exception: return None def _get_whisper(): global _whisper if _whisper is None: with _lock: if _whisper is None: from faster_whisper import WhisperModel _whisper = WhisperModel( WHISPER_MODEL, device=WHISPER_DEVICE, compute_type=WHISPER_COMPUTE, ) return _whisper def _whisper_transcribe(audio_path: str) -> str: model = _get_whisper() segments, _info = model.transcribe( audio_path, language=WHISPER_LANG, beam_size=WHISPER_BEAM, vad_filter=True, ) return " ".join(seg.text.strip() for seg in segments).strip() def transcribe(audio_path: str) -> str: """Transcribe an audio file to plain text. Returns '' for no/empty input. Order: llama.cpp (Gemma mtmd) -> Gemma transformers -> faster-whisper. The Gemma paths only run for clips within the 30s audio cap; longer notes go straight to whisper, which handles any length.""" if not audio_path: return "" short = (dur := _duration(audio_path)) is None or dur <= GEMMA_AUDIO_MAX_SEC if short: # Primary: llama.cpp mtmd (Gemma audio encoder) try: text = llama_backend.transcribe_audio(audio_path) if text: return text except Exception: pass # Fallback: Gemma via transformers try: text = gemma.transcribe_audio(audio_path) if text: return text except Exception: pass # Last resort (and all long audio): faster-whisper return _whisper_transcribe(audio_path) if __name__ == "__main__": # quick CLI: python transcriber.py path/to/audio.m4a import sys if len(sys.argv) < 2: print("usage: python transcriber.py ") raise SystemExit(1) print(transcribe(sys.argv[1]))