import io import threading import wave from django.conf import settings 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 = {} _lock = threading.Lock() def _get_voice(lang): if lang not in _voices: with _lock: if lang not in _voices: from piper import PiperVoice _voices[lang] = PiperVoice.load(str(settings.TTS_VOICES[lang])) return _voices[lang] def synthesize_wav(text, lang=DEFAULT_LANGUAGE): """Synthesize `text` (in `lang`) to WAV bytes. 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. """ voice = _get_voice(lang) buf = io.BytesIO() with _lock: with wave.open(buf, 'wb') as wav_file: voice.synthesize_wav(text, wav_file) return buf.getvalue()