Spaces:
Sleeping
Sleeping
Commit ·
96a2a78
1
Parent(s): 7aba3a7
Two-level TTS cache: raw samples + encoded per format
Browse filesCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- services/tts_service.py +40 -11
services/tts_service.py
CHANGED
|
@@ -49,6 +49,13 @@ _cache: "OrderedDict[tuple, tuple[bytes, str]]" = OrderedDict()
|
|
| 49 |
_cache_lock = threading.Lock()
|
| 50 |
_CACHE_MAX = int(os.getenv("TTS_CACHE_SIZE", "256"))
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
def _onnx_paths() -> tuple[Path, Path]:
|
| 54 |
model = Path(os.getenv("KOKORO_ONNX_MODEL", "models/kokoro-v1.0.onnx"))
|
|
@@ -218,6 +225,22 @@ def _cache_put(key: tuple, value: tuple[bytes, str]) -> None:
|
|
| 218 |
_cache.popitem(last=False)
|
| 219 |
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
def _encode(samples, fmt: str) -> tuple[bytes, str]:
|
| 222 |
"""Encode float32 samples → (bytes, mime). OGG/Opus is ~10x smaller than
|
| 223 |
WAV for speech; falls back to WAV if libsndfile lacks Opus support."""
|
|
@@ -244,22 +267,28 @@ def synthesize(text: str, voice: str | None = None, fmt: str = "wav") -> tuple[b
|
|
| 244 |
voice = voice or DEFAULT_VOICE
|
| 245 |
fmt = "ogg" if str(fmt).lower() in ("ogg", "opus") else "wav"
|
| 246 |
key = (text, voice, fmt)
|
|
|
|
| 247 |
|
| 248 |
cached = _cache_get(key)
|
| 249 |
if cached is not None:
|
| 250 |
return cached
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
_cache_put(key, result)
|
| 264 |
return result
|
| 265 |
|
|
|
|
| 49 |
_cache_lock = threading.Lock()
|
| 50 |
_CACHE_MAX = int(os.getenv("TTS_CACHE_SIZE", "256"))
|
| 51 |
|
| 52 |
+
# Second cache level: raw float32 samples per (text, voice). Synthesis is the
|
| 53 |
+
# expensive step (seconds); encoding is milliseconds — so a request for the
|
| 54 |
+
# same text in a different container (e.g. Safari asking for wav after the
|
| 55 |
+
# cache was warmed with ogg) re-encodes instead of re-synthesizing.
|
| 56 |
+
_sample_cache: "OrderedDict[tuple, object]" = OrderedDict()
|
| 57 |
+
_SAMPLE_CACHE_MAX = int(os.getenv("TTS_SAMPLE_CACHE_SIZE", "128"))
|
| 58 |
+
|
| 59 |
|
| 60 |
def _onnx_paths() -> tuple[Path, Path]:
|
| 61 |
model = Path(os.getenv("KOKORO_ONNX_MODEL", "models/kokoro-v1.0.onnx"))
|
|
|
|
| 225 |
_cache.popitem(last=False)
|
| 226 |
|
| 227 |
|
| 228 |
+
def _samples_get(key: tuple):
|
| 229 |
+
with _cache_lock:
|
| 230 |
+
hit = _sample_cache.get(key)
|
| 231 |
+
if hit is not None:
|
| 232 |
+
_sample_cache.move_to_end(key)
|
| 233 |
+
return hit
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def _samples_put(key: tuple, samples) -> None:
|
| 237 |
+
with _cache_lock:
|
| 238 |
+
_sample_cache[key] = samples
|
| 239 |
+
_sample_cache.move_to_end(key)
|
| 240 |
+
while len(_sample_cache) > _SAMPLE_CACHE_MAX:
|
| 241 |
+
_sample_cache.popitem(last=False)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
def _encode(samples, fmt: str) -> tuple[bytes, str]:
|
| 245 |
"""Encode float32 samples → (bytes, mime). OGG/Opus is ~10x smaller than
|
| 246 |
WAV for speech; falls back to WAV if libsndfile lacks Opus support."""
|
|
|
|
| 267 |
voice = voice or DEFAULT_VOICE
|
| 268 |
fmt = "ogg" if str(fmt).lower() in ("ogg", "opus") else "wav"
|
| 269 |
key = (text, voice, fmt)
|
| 270 |
+
skey = (text, voice)
|
| 271 |
|
| 272 |
cached = _cache_get(key)
|
| 273 |
if cached is not None:
|
| 274 |
return cached
|
| 275 |
|
| 276 |
+
samples = _samples_get(skey)
|
| 277 |
+
if samples is None:
|
| 278 |
+
with _synth_sem:
|
| 279 |
+
cached = _cache_get(key) # may have been filled while we waited
|
| 280 |
+
if cached is not None:
|
| 281 |
+
return cached
|
| 282 |
+
samples = _samples_get(skey)
|
| 283 |
+
if samples is None:
|
| 284 |
+
backend = _get_backend()
|
| 285 |
+
chunks = list(backend.segments(text, voice))
|
| 286 |
+
if not chunks:
|
| 287 |
+
raise RuntimeError("Kokoro produced no audio")
|
| 288 |
+
samples = np.concatenate(chunks) if len(chunks) > 1 else chunks[0]
|
| 289 |
+
_samples_put(skey, samples)
|
| 290 |
+
|
| 291 |
+
result = _encode(samples, fmt)
|
| 292 |
_cache_put(key, result)
|
| 293 |
return result
|
| 294 |
|