diora-web/tts/views.py

58 lines
2 KiB
Python
Raw Normal View History

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