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) try: audio = _synth_pool.apply(piper_engine.synthesize_wav, (text,)) 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