2026-08-31 22:29:13 +02:00
|
|
|
import io
|
|
|
|
|
import threading
|
|
|
|
|
import wave
|
|
|
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
2026-09-01 07:04:27 +02:00
|
|
|
DEFAULT_LANGUAGE = 'de'
|
|
|
|
|
SUPPORTED_LANGUAGES = tuple(settings.TTS_VOICES.keys())
|
|
|
|
|
|
|
|
|
|
# Lazy per-process singletons, one per language: each gunicorn worker loads a
|
|
|
|
|
# voice only once it's actually requested, rather than all workers loading
|
|
|
|
|
# every model at startup (the host runs several other containers with limited
|
|
|
|
|
# spare RAM). One lock guards both the lazy-load and the inference call below
|
|
|
|
|
# — a single self-hosted user never needs concurrent synthesis across
|
|
|
|
|
# languages, so there's no reason for a lock per voice.
|
|
|
|
|
_voices = {}
|
2026-08-31 22:29:13 +02:00
|
|
|
_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
2026-09-01 07:04:27 +02:00
|
|
|
def _get_voice(lang):
|
|
|
|
|
if lang not in _voices:
|
2026-08-31 22:29:13 +02:00
|
|
|
with _lock:
|
2026-09-01 07:04:27 +02:00
|
|
|
if lang not in _voices:
|
2026-08-31 22:29:13 +02:00
|
|
|
from piper import PiperVoice
|
2026-09-01 07:04:27 +02:00
|
|
|
_voices[lang] = PiperVoice.load(str(settings.TTS_VOICES[lang]))
|
|
|
|
|
return _voices[lang]
|
2026-08-31 22:29:13 +02:00
|
|
|
|
|
|
|
|
|
2026-09-01 07:04:27 +02:00
|
|
|
def synthesize_wav(text, lang=DEFAULT_LANGUAGE):
|
|
|
|
|
"""Synthesize `text` (in `lang`) to WAV bytes.
|
2026-08-31 22:29:13 +02:00
|
|
|
|
|
|
|
|
Never persists or logs `text` — callers must not log it either. The lock
|
|
|
|
|
also serializes inference, since one onnxruntime session isn't meant to
|
|
|
|
|
run concurrent calls within a process.
|
|
|
|
|
"""
|
2026-09-01 07:04:27 +02:00
|
|
|
voice = _get_voice(lang)
|
2026-08-31 22:29:13 +02:00
|
|
|
buf = io.BytesIO()
|
|
|
|
|
with _lock:
|
|
|
|
|
with wave.open(buf, 'wb') as wav_file:
|
|
|
|
|
voice.synthesize_wav(text, wav_file)
|
|
|
|
|
return buf.getvalue()
|