Spaces:
Sleeping
Sleeping
| """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 <audio-file>") | |
| raise SystemExit(1) | |
| print(transcribe(sys.argv[1])) | |