1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
"""Optional local Whisper transcription of reference audio."""
import logging
import threading
from typing import Dict, Optional, Tuple
logger = logging.getLogger(__name__)
# One loaded Whisper model per (backend, model size), reused across calls.
# The audio.cpp setup transcribes every reference wav in one run; loading
# the model once instead of per file saves seconds per voice. The lock
# keeps a TUI lane and a concurrent conversion from racing the load.
_MODEL_LOCK = threading.Lock()
_MODELS: Dict[Tuple[str, str], object] = {}
def _cached_model(key: Tuple[str, str], loader):
"""The model for KEY, loaded by LOADER() on first use and cached."""
with _MODEL_LOCK:
model = _MODELS.get(key)
if model is None:
model = loader()
_MODELS[key] = model
return model
def _import_failure_reason(backend: str, exc: ImportError) -> str:
"""Why importing BACKEND failed: missing package or broken install.
A backend whose own compiled dependency fails to load raises the same
ImportError as a missing package (e.g. faster-whisper whose av build
cannot load its shared libraries); telling the two apart turns 'was it
even installed?' into the actual fix. A ModuleNotFoundError naming the
backend itself is a plain missing package; anything else is checked
against the installed distributions.
"""
if isinstance(exc, ModuleNotFoundError) \
and getattr(exc, "name", "") == backend:
return f"{backend} is not installed"
try:
import importlib.util
present = importlib.util.find_spec(backend) is not None
except (ImportError, ValueError):
present = True
if not present:
return f"{backend} is not installed"
return f"{backend} is installed but failed to import: {exc}"
def _transcribe_with(backend: str, audio_path: str,
model_name: str) -> Tuple[Optional[str], str]:
"""One transcription attempt with BACKEND; returns (text, reason).
TEXT is the transcript, or None on any failure; REASON then explains
why in one human-readable line (missing package, broken import, model
or transcribe error, or an empty result), so callers can surface the
cause instead of a bare 'no transcript'.
"""
try:
if backend == "faster_whisper":
def load():
from faster_whisper import WhisperModel
return WhisperModel(model_name, device="cpu", compute_type="int8")
model = _cached_model(("faster_whisper", model_name), load)
segments, _ = model.transcribe(audio_path)
text = " ".join(seg.text.strip() for seg in segments).strip()
else:
def load():
import whisper
return whisper.load_model(model_name)
model = _cached_model(("whisper", model_name), load)
result = model.transcribe(audio_path)
text = (result.get("text") or "").strip()
except ImportError as exc:
return None, _import_failure_reason(backend, exc)
except Exception as exc:
return None, f"{backend} transcription failed: {exc}"
if not text:
return None, f"{backend} heard no speech in this audio"
logger.info("Transcription complete via %s: %s", backend, text)
return text, "ok"
def transcribe_reference_audio_detailed(
audio_path: str, model_name: str = "base") -> Tuple[Optional[str], str]:
"""Transcribe one reference wav locally; returns (text, reason).
The current qwen-tts demo does not expose a transcription endpoint, so
transcription is done client-side when a Whisper backend is available.
TEXT is the transcript, or None when no backend produced one; REASON
is "ok" on success and otherwise explains the failure (tried backends
in order), e.g. "faster_whisper is installed but failed to import:
...; whisper is not installed" — what the audio.cpp setup prints so a
voice that cannot be transcribed is never a silent blank.
"""
reasons = []
for backend in ("faster_whisper", "whisper"):
text, reason = _transcribe_with(backend, audio_path, model_name)
if text:
return text, "ok"
reasons.append(reason)
return None, "; ".join(reasons)
def transcribe_reference_audio(audio_path: str, model_name: str = "base") -> Optional[str]:
"""Transcribe reference audio locally using an optional Whisper backend.
Returns None if no backend is installed or transcription failed;
transcribe_reference_audio_detailed also explains why.
"""
text, _ = transcribe_reference_audio_detailed(audio_path, model_name)
return text
def whisper_backend_available() -> Optional[str]:
"""Return the name of an importable Whisper backend, or None.
Checks faster_whisper first (preferred), then the openai-whisper
package, without importing the heavy model code: a bare import probe
is enough to tell whether the package is installed in the current
environment. Used by the make_audiocpp_server_json tool to warn when
neither is present (e.g. the wrong conda environment is active).
"""
for backend in ("faster_whisper", "whisper"):
try:
__import__(backend)
except ImportError:
continue
return backend
return None
def whisper_backend_problem() -> Optional[str]:
"""None when a Whisper backend is importable, else why none is usable.
One line per backend in probe order, distinguishing 'not installed'
from 'installed but failed to import: <error>' — the setup prints this
before transcribing so a broken compiled dependency (which surfaces as
the same ImportError as a missing package) is visible as such.
"""
problems = []
for backend in ("faster_whisper", "whisper"):
try:
__import__(backend)
except ImportError as exc:
problems.append(_import_failure_reason(backend, exc))
except Exception as exc:
problems.append(f"{backend} failed to import: {exc}")
else:
return None
return "; ".join(problems)
|