diff --git a/speech.py b/speech.py index e1dbdfa..0a87bce 100755 --- a/speech.py +++ b/speech.py @@ -16,6 +16,7 @@ from fastapi.responses import StreamingResponse from loguru import logger from openedai import OpenAIStub, BadRequestError, ServiceUnavailableError from pydantic import BaseModel +from typing import Optional import uvicorn @contextlib.asynccontextmanager @@ -38,6 +39,9 @@ kokoro_pipeline = None kokoro_lang = None args = None +# Voice-to-model lookup cache (loaded at startup) +voice_to_model_cache = {} + def unload_model(): import torch, gc global xtts @@ -246,6 +250,15 @@ def preprocess(raw_input): #logger.debug(f"preprocess: after: {[raw_input]}") return raw_input +# Auto-detect which model a voice belongs to (uses cached mapping) +def detect_model_from_voice(voice: str) -> str: + """Find which model supports a given voice name. + Returns the first model that has this voice, or None if not found. + Uses voice_to_model_cache populated at startup for fast lookups. + """ + global voice_to_model_cache + return voice_to_model_cache.get(voice, None) + # Read voice map on demand so it can be changed without restarting the server def map_voice_to_speaker(voice: str, model: str): default_exists('config/voice_to_speaker.yaml') @@ -258,7 +271,7 @@ def map_voice_to_speaker(voice: str, model: str): raise BadRequestError(f"Error loading voice: {voice}, KeyError: {e}", param='voice') class GenerateSpeechRequest(BaseModel): - model: str = "tts-1" # or "tts-1-hd" + model: Optional[str] = None # Auto-detected from voice if not provided input: str voice: str = "alloy" # alloy, echo, fable, onyx, nova, and shimmer response_format: str = "mp3" # mp3, opus, aac, flac @@ -288,7 +301,41 @@ def build_ffmpeg_args(response_format, input_format, sample_rate): @app.get("/v1/models") async def list_models(): - """List all available TTS models and their supported voices""" + """List all available TTS models (OpenAI-compatible format)""" + # Return minimal OpenAI-compatible model list (no extra fields) + return { + "object": "list", + "data": [ + { + "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" + } + ] + } + +@app.get("/v1/voices") +async def list_voices(): + """List all available voices with model mapping and metadata (extended endpoint)""" default_exists('config/voice_to_speaker.yaml') with open('config/voice_to_speaker.yaml', 'r', encoding='utf8') as file: @@ -300,11 +347,11 @@ async def list_models(): if isinstance(voices, dict): voice_list = list(voices.keys()) - # Add model metadata + # Add model metadata with extended info model_info = { "id": model_id, "object": "model", - "created": 1700000000, # Static timestamp + "created": 1700000000, "owned_by": "uncloseai", "voices": voice_list, "voice_count": len(voice_list) @@ -351,6 +398,15 @@ async def generate_speech(request: GenerateSpeechRequest): response_format = request.response_format.lower() speed = request.speed + # Auto-detect model from voice if model not provided + if model is None: + detected_model = detect_model_from_voice(voice) + if detected_model: + logger.info(f"Auto-detected model '{detected_model}' for voice '{voice}'") + model = detected_model + else: + raise BadRequestError(f"Voice '{voice}' not found in any model. Please specify a model.", param='voice') + # Set the Content-Type header based on the requested format if response_format == "mp3": media_type = "audio/mpeg" @@ -665,6 +721,17 @@ if __name__ == "__main__": default_exists('config/pre_process_map.yaml') default_exists('config/voice_to_speaker.yaml') + # Build voice-to-model cache for fast lookups + 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 isinstance(voices, dict): + for voice_name in voices.keys(): + # First match wins (for duplicate voice names across models) + if voice_name not in voice_to_model_cache: + voice_to_model_cache[voice_name] = model_id + print(f"Voice-to-model cache initialized with {len(voice_to_model_cache)} voices") + logger.remove() logger.add(sink=sys.stderr, level=args.log_level)