🦝 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:
Russell Ballestrini 2025-11-09 18:13:52 -05:00
parent 549a35d613
commit bc2071900c

115
speech.py
View file

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