Reader-Header bekommt ein #reader-tts-lang-Dropdown (Auswahl, keine
Sprach-Erkennung — persistiert in localStorage); /tts/synthesize/ nimmt jetzt
{"lang": "de"|"en"} entgegen. tts/piper_engine.py lädt pro Sprache eine eigene
PiperVoice lazy (settings.TTS_VOICES), Docker-Image lädt zusätzlich
en_US-lessac-medium beim Build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb6yX2S9aTGepYA9JFba9x
57 lines
2 KiB
Python
57 lines
2 KiB
Python
import json
|
|
|
|
from django.http import JsonResponse, StreamingHttpResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_http_methods
|
|
from gevent.threadpool import ThreadPool
|
|
|
|
from . import piper_engine
|
|
|
|
# One sentence per request, hard-capped — this is the load-bearing part of the
|
|
# "server never holds more than a small, transient snippet of book text"
|
|
# agreement (see CLAUDE.md), not just a client-side convention.
|
|
MAX_TEXT_LENGTH = 500
|
|
|
|
# Offloads the CPU-bound Piper inference off the gevent hub's event loop, so a
|
|
# synthesis call doesn't stall other concurrent greenlets (radio SSE, other
|
|
# requests) in the same worker the way a plain in-greenlet call would.
|
|
_synth_pool = ThreadPool(1)
|
|
|
|
|
|
def _require_auth(request):
|
|
if not request.user.is_authenticated:
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
|
return None
|
|
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(['POST'])
|
|
def synthesize(request):
|
|
err = _require_auth(request)
|
|
if err:
|
|
return err
|
|
|
|
try:
|
|
body = json.loads(request.body)
|
|
except (json.JSONDecodeError, ValueError):
|
|
return JsonResponse({'error': 'invalid JSON'}, status=400)
|
|
|
|
text = body.get('text', '')
|
|
if not isinstance(text, str) or not text.strip():
|
|
return JsonResponse({'error': 'text required'}, status=400)
|
|
if len(text) > MAX_TEXT_LENGTH:
|
|
return JsonResponse({'error': f'text exceeds {MAX_TEXT_LENGTH} characters'}, status=400)
|
|
|
|
lang = body.get('lang', piper_engine.DEFAULT_LANGUAGE)
|
|
if lang not in piper_engine.SUPPORTED_LANGUAGES:
|
|
return JsonResponse({'error': 'unsupported lang'}, status=400)
|
|
|
|
try:
|
|
audio = _synth_pool.apply(piper_engine.synthesize_wav, (text, lang))
|
|
except Exception:
|
|
return JsonResponse({'error': 'synthesis failed'}, status=500)
|
|
|
|
response = StreamingHttpResponse(iter([audio]), content_type='audio/wav')
|
|
response['Cache-Control'] = 'no-store'
|
|
response['X-Accel-Buffering'] = 'no'
|
|
return response
|