diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4f0a3..654e33f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,9 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi - **Docker Socket Isolation.** Compose deployments now place the host Docker socket behind a dedicated network proxy; the web container no longer mounts the socket or joins the host Docker group. The client retains direct Unix-socket compatibility for deployments that intentionally configure it. -- **TTS Queue Bound.** Preview, style-variation, and seed-finder synthesis now share a two-slot process-local queue; - excess requests receive a retryable 429 after a bounded wait instead of creating unbounded worker threads. +- **TTS Queue Bound.** Preview, style-variation, seed-finder, and OpenAI-compatible speech-proxy synthesis now share + a two-slot process-local queue; excess requests receive a retryable 429 after a bounded wait instead of creating + unbounded worker threads. - **STT Queue Bound.** Transcription and STT benchmark calls now share a single bounded process-local slot, preventing concurrent recognition requests from overcommitting the inference device. - **Visual Identity Versioning.** Consistent profile/reference results now carry the standardized v3 prompt marker, diff --git a/TODO.md b/TODO.md index f99da6a..f8c53d6 100644 --- a/TODO.md +++ b/TODO.md @@ -121,8 +121,8 @@ the first block before investing in architecture or polish. - [ ] Prevent duplicate generation when a user clicks twice. - [ ] Centralize provider health checks and error handling. - [ ] Limit concurrency to 1โ€“2 image generations, 1โ€“2 voice generations, and 1 STT request per user. Consistent - portrait/reference rendering and preview/style/seed-finder synthesis are capped at two active jobs; extend the - cap to full speech-proxy paths; STT transcription and benchmarks are capped at one active job. + portrait/reference rendering, preview/style/seed-finder synthesis, and the OpenAI-compatible speech proxy are + capped at two active jobs; STT transcription and benchmarks are capped at one active job. - [ ] Use bounded server queues. ## P0 โ€” Security diff --git a/routes/tts.py b/routes/tts.py index da3ae1a..52e47d7 100644 --- a/routes/tts.py +++ b/routes/tts.py @@ -696,6 +696,8 @@ async def tts_preview(request: Request): ) else: audio, media_type = await _bounded_tts_call(_preview_request_audio, text, voice, settings, instruct, backend) + except HTTPException: + raise except Exception as e: raise HTTPException(502, f"TTS error: {e}") audio, media_type, ext, duration_sec, clipped = _prepare_proxy_audio(audio, media_type, response_format) @@ -733,6 +735,8 @@ async def tts_style_variation(request: Request): try: audio, media_type = await _bounded_tts_call(_preview_request_audio, text, source_voice, settings, instruct, backend) audio, media_type, _ext, duration, _clipped = _prepare_proxy_audio(audio, media_type, "wav") + except HTTPException: + raise except Exception as e: raise HTTPException(502, f"Style variation synthesis failed: {e}") @@ -959,7 +963,9 @@ async def openai_speech_proxy(request: Request): if backend == "streaming" and not virtual and response_format == "wav" and not route_has_sounds and not route_has_speed: try: - resp = await asyncio.to_thread(_open_tts_stream_response, text, voice, settings, style_instruction) + resp = await _bounded_tts_call(_open_tts_stream_response, text, voice, settings, style_instruction) + except HTTPException: + raise except Exception as e: _routing_log_request( request, status="error", app=request_app, requested_voice=original_voice, @@ -996,43 +1002,45 @@ async def openai_speech_proxy(request: Request): instruct = str(preset.get("description", "")).strip() if not instruct: raise RuntimeError(f"Virtual voice '{voice}' has no description") - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _voice_design_request_audio, instruct, text, str(preset.get("language", "Auto")), settings, str(preset.get("gender", "")), ) elif backend == "voice_design": - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _voice_design_voice_request_audio, voice, text, settings, style_instruction, str(data.get("language") or "Auto"), ) elif backend == "streaming": - resp = await asyncio.to_thread(_open_tts_stream_response, text, voice, settings, style_instruction) - audio, media_type = await asyncio.to_thread(_read_tts_stream_response, resp) + resp = await _bounded_tts_call(_open_tts_stream_response, text, voice, settings, style_instruction) + audio, media_type = await _bounded_tts_call(_read_tts_stream_response, resp) if not audio: raise RuntimeError("backend returned empty audio") elif backend == "nvidia_magpie": - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _tts_request_audio, text, voice, settings, style_instruction, _preview_backend_base_url(settings, "nvidia_magpie"), settings.get("tts_api_key", ""), "nvidia_magpie", "nvidia_magpie", ) elif backend in {"nvidia_zeroshot", "nvidia_flow"}: - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _nvidia_clone_request_audio, text, voice, settings, "flow" if backend == "nvidia_flow" else "zeroshot", str(data.get("audio_prompt_transcript") or ""), str(data.get("language") or ""), ) elif backend == "fishspeech": - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _fishspeech_request_audio, text, voice, settings, style_instruction, str(data.get("language") or ""), ) else: - audio, media_type = await asyncio.to_thread( + audio, media_type = await _bounded_tts_call( _tts_request_audio, text, voice, settings, style_instruction, ) + except HTTPException: + raise except Exception as e: _routing_log_request( request, status="error", app=request_app, requested_voice=original_voice,