From e9c978b051315bd54a3fc1462136727028ec7b0d Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 24 May 2026 11:58:35 -0400 Subject: [PATCH] endpoints: filter /v1/models and /v1/voices by engine availability flags --- speech.py | 73 ++++++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/speech.py b/speech.py index 2747069..69360e5 100755 --- a/speech.py +++ b/speech.py @@ -66,6 +66,17 @@ except ImportError: torch = None F5TTS = None +def is_engine_available(model_id): + """Whether the TTS engine for a given model_id is loadable in this process. + Used to filter advertised models/voices so we don't 503 on requests for + engines whose Python deps weren't installed. tts-1/tts-1-hd/tts-1-silero/ + tts-1-kokoro are assumed available — they surface their own load errors.""" + if model_id == 'tts-1-qwen': + return QWEN_TTS_AVAILABLE + if model_id == 'tts-1-f5': + return F5_TTS_AVAILABLE + return True + @contextlib.asynccontextmanager async def lifespan(app): # Startup: Initialize voice caches in each worker process @@ -74,10 +85,12 @@ async def lifespan(app): default_exists('config/pre_process_map.yaml') default_exists('config/voice_to_speaker.yaml') - # Build voice-to-model cache for fast lookups + # Build voice-to-model cache for fast lookups (skip engines we can't load) with open('config/voice_to_speaker.yaml', 'r', encoding='utf8') as file: voice_map = yaml.safe_load(file) for model_id, voices in voice_map.items(): + if not is_engine_available(model_id): + continue if isinstance(voices, dict): for voice_name in voices.keys(): # First match wins (for duplicate voice names across models) @@ -85,9 +98,11 @@ async def lifespan(app): voice_to_model_cache[voice_name] = model_id print(f"Voice-to-model cache initialized with {len(voice_to_model_cache)} voices") - # Build voices cache for /v1/voices endpoint + # Build voices cache for /v1/voices endpoint (skip engines we can't load) models_data = [] for model_id, voices in voice_map.items(): + if not is_engine_available(model_id): + continue if isinstance(voices, dict): voice_list = list(voices.keys()) @@ -731,51 +746,21 @@ def build_ffmpeg_args(response_format, input_format, sample_rate): @app.get("/v1/models") async def list_models(): - """List all available TTS models (OpenAI-compatible format)""" - # Return minimal OpenAI-compatible model list (no extra fields) - # tts-1-qwen and tts-1-f5 enabled by default - return { - "object": "list", - "data": [ - { - "id": "tts-1-qwen", + """List all available TTS models (OpenAI-compatible format). + + Only advertise engines whose Python deps are actually loadable — otherwise + clients try a model and get 503, which they treat as a transient outage. + """ + available = [] + for model_id in ('tts-1-qwen', 'tts-1-f5'): + if is_engine_available(model_id): + available.append({ + "id": model_id, "object": "model", "created": 1700000000, "owned_by": "uncloseai" - }, - { - "id": "tts-1-f5", - "object": "model", - "created": 1700000000, - "owned_by": "uncloseai" - } - # Other models disabled by default: - # { - # "id": "tts-1", - # "object": "model", - # "created": 1700000000, - # "owned_by": "uncloseai" - # }, - # { - # "id": "tts-1-hd", - # "object": "model", - # "created": 1700000000, - # "owned_by": "uncloseai" - # }, - # { - # "id": "tts-1-silero", - # "object": "model", - # "created": 1700000000, - # "owned_by": "uncloseai" - # }, - # { - # "id": "tts-1-kokoro", - # "object": "model", - # "created": 1700000000, - # "owned_by": "uncloseai" - # } - ] - } + }) + return {"object": "list", "data": available} @app.get("/v1/voices") async def list_voices():