perf: bound speech proxy synthesis

This commit is contained in:
mARTin-B78 2026-09-04 08:00:41 +02:00
parent 9517cca00c
commit 760a3640ba
3 changed files with 22 additions and 13 deletions

View File

@ -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,

View File

@ -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

View File

@ -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,