🦝 Fix voice cache initialization in multiprocess workers
**Problem:** Voice-to-model cache was only initialized in parent process, not in worker processes spawned by uvicorn workers=4. This caused ALL voice auto-detection to fail with "Voice not found in any model" errors. **Root Cause:** Cache initialization was in `if __name__ == "__main__"` block, which only runs in the parent process. Worker processes import the `app` object directly and don't execute the __main__ block. **Solution:** Moved cache initialization to FastAPI `lifespan` context manager, which runs during startup in EACH worker process. This ensures every worker has the voice_to_model_cache and voices_cache populated. **Impact:** - Voice auto-detection now works in all 4 worker processes - /v1/voices endpoint returns cached data in all workers - All 227 voices can now be used without specifying model parameter 🦝 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
549a35d613
commit
bc2071900c
1 changed files with 60 additions and 55 deletions
115
speech.py
115
speech.py
|
|
@ -22,7 +22,67 @@ import uvicorn
|
|||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup: Initialize voice caches in each worker process
|
||||
global voice_to_model_cache, voices_cache
|
||||
|
||||
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")
|
||||
|
||||
# Build voices cache for /v1/voices endpoint
|
||||
models_data = []
|
||||
for model_id, voices in voice_map.items():
|
||||
if isinstance(voices, dict):
|
||||
voice_list = list(voices.keys())
|
||||
|
||||
model_info = {
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": 1700000000,
|
||||
"owned_by": "uncloseai",
|
||||
"voices": voice_list,
|
||||
"voice_count": len(voice_list)
|
||||
}
|
||||
|
||||
# Add engine-specific metadata
|
||||
if model_id == 'tts-1':
|
||||
model_info["engine"] = "piper"
|
||||
model_info["description"] = "Fast neural TTS with 100+ voices"
|
||||
model_info["sample_rate"] = 22050
|
||||
elif model_id == 'tts-1-hd':
|
||||
model_info["engine"] = "xtts"
|
||||
model_info["description"] = "High-quality voice cloning TTS"
|
||||
model_info["sample_rate"] = 24000
|
||||
elif model_id == 'tts-1-silero':
|
||||
model_info["engine"] = "silero"
|
||||
model_info["description"] = "Fast multilingual TTS (en, ru, de, es, fr)"
|
||||
model_info["sample_rate"] = 48000
|
||||
elif model_id == 'tts-1-kokoro':
|
||||
model_info["engine"] = "kokoro"
|
||||
model_info["description"] = "Lightweight decoder-only TTS (82M params)"
|
||||
model_info["sample_rate"] = 24000
|
||||
|
||||
models_data.append(model_info)
|
||||
|
||||
voices_cache = {
|
||||
"object": "list",
|
||||
"data": models_data
|
||||
}
|
||||
print(f"/v1/voices cache initialized with {len(models_data)} models")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown: Cleanup
|
||||
gc.collect()
|
||||
try:
|
||||
import torch
|
||||
|
|
@ -743,61 +803,6 @@ if __name__ == "__main__":
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
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")
|
||||
|
||||
# Build voices cache for /v1/voices endpoint
|
||||
models_data = []
|
||||
for model_id, voices in voice_map.items():
|
||||
if isinstance(voices, dict):
|
||||
voice_list = list(voices.keys())
|
||||
|
||||
model_info = {
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": 1700000000,
|
||||
"owned_by": "uncloseai",
|
||||
"voices": voice_list,
|
||||
"voice_count": len(voice_list)
|
||||
}
|
||||
|
||||
# Add engine-specific metadata
|
||||
if model_id == 'tts-1':
|
||||
model_info["engine"] = "piper"
|
||||
model_info["description"] = "Fast neural TTS with 100+ voices"
|
||||
model_info["sample_rate"] = 22050
|
||||
elif model_id == 'tts-1-hd':
|
||||
model_info["engine"] = "xtts"
|
||||
model_info["description"] = "High-quality voice cloning TTS"
|
||||
model_info["sample_rate"] = 24000
|
||||
elif model_id == 'tts-1-silero':
|
||||
model_info["engine"] = "silero"
|
||||
model_info["description"] = "Fast multilingual TTS (en, ru, de, es, fr)"
|
||||
model_info["sample_rate"] = 48000
|
||||
elif model_id == 'tts-1-kokoro':
|
||||
model_info["engine"] = "kokoro"
|
||||
model_info["description"] = "Lightweight decoder-only TTS (82M params)"
|
||||
model_info["sample_rate"] = 24000
|
||||
|
||||
models_data.append(model_info)
|
||||
|
||||
voices_cache = {
|
||||
"object": "list",
|
||||
"data": models_data
|
||||
}
|
||||
print(f"/v1/voices cache initialized with {len(models_data)} models")
|
||||
|
||||
logger.remove()
|
||||
logger.add(sink=sys.stderr, level=args.log_level)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue