This commit is contained in:
Timothy Jaeryang Baek
2026-04-17 13:04:07 +09:00
parent e709d6812f
commit 34d569d564
+93 -78
View File
@@ -5,7 +5,6 @@ import os
import uuid
import html
import base64
from functools import lru_cache
from pydub import AudioSegment
from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor
@@ -421,7 +420,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
voice_id = payload.get('voice', '')
if voice_id not in get_available_voices(request):
if voice_id not in await get_available_voices(request):
raise HTTPException(
status_code=400,
detail='Invalid voice id',
@@ -1295,40 +1294,53 @@ async def transcription(
)
def get_available_models(request: Request) -> list[dict]:
async def get_available_models(request: Request) -> list[dict]:
available_models = []
if request.app.state.config.TTS_ENGINE == 'openai':
# Use custom endpoint if not using the official OpenAI API URL
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'):
try:
response = requests.get(
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models',
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
)
response.raise_for_status()
data = response.json()
available_models = data.get('models', [])
except Exception as e:
log.error(f'Error fetching models from custom endpoint: {str(e)}')
available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}]
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
try:
async with session.get(
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models',
) as response:
response.raise_for_status()
data = await response.json()
available_models = data.get('models', [])
except Exception as e:
log.debug(f'/audio/models not available, trying /models fallback: {str(e)}')
# Fallback to standard OpenAI-compatible /models endpoint
# (used by KokoroTTS and similar custom TTS servers)
try:
async with session.get(
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/models',
) as response:
response.raise_for_status()
data = await response.json()
# OpenAI /models returns {"data": [...]}, /audio/models returns {"models": [...]}
available_models = data.get('data', data.get('models', []))
except Exception as e2:
log.error(f'Error fetching models from custom endpoint: {str(e2)}')
available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}]
else:
available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}]
elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
try:
response = requests.get(
f'{ELEVENLABS_API_BASE_URL}/v1/models',
headers={
'xi-api-key': request.app.state.config.TTS_API_KEY,
'Content-Type': 'application/json',
},
timeout=5,
)
response.raise_for_status()
models = response.json()
available_models = [{'name': model['name'], 'id': model['model_id']} for model in models]
except requests.RequestException as e:
log.error(f'Error fetching voices: {str(e)}')
timeout = aiohttp.ClientTimeout(total=5)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
f'{ELEVENLABS_API_BASE_URL}/v1/models',
headers={
'xi-api-key': request.app.state.config.TTS_API_KEY,
'Content-Type': 'application/json',
},
) as response:
response.raise_for_status()
models = await response.json()
available_models = [{'name': model['name'], 'id': model['model_id']} for model in models]
except Exception as e:
log.error(f'Error fetching models: {str(e)}')
elif request.app.state.config.TTS_ENGINE == 'mistral':
available_models = [{'id': 'mistral-tts-latest'}]
return available_models
@@ -1336,24 +1348,25 @@ def get_available_models(request: Request) -> list[dict]:
@router.get('/models')
async def get_models(request: Request, user=Depends(get_verified_user)):
return {'models': get_available_models(request)}
return {'models': await get_available_models(request)}
def get_available_voices(request) -> dict:
async def get_available_voices(request) -> dict:
"""Returns {voice_id: voice_name} dict"""
available_voices = {}
if request.app.state.config.TTS_ENGINE == 'openai':
# Use custom endpoint if not using the official OpenAI API URL
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'):
try:
response = requests.get(
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices',
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
)
response.raise_for_status()
data = response.json()
voices_list = data.get('voices', [])
available_voices = {voice['id']: voice['name'] for voice in voices_list}
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices',
) as response:
response.raise_for_status()
data = await response.json()
voices_list = data.get('voices', [])
available_voices = {voice['id']: voice['name'] for voice in voices_list}
except Exception as e:
log.error(f'Error fetching voices from custom endpoint: {str(e)}')
available_voices = {
@@ -1375,7 +1388,7 @@ def get_available_voices(request) -> dict:
}
elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
try:
available_voices = get_elevenlabs_voices(api_key=request.app.state.config.TTS_API_KEY)
available_voices = await get_elevenlabs_voices(api_key=request.app.state.config.TTS_API_KEY)
except Exception:
# Avoided @lru_cache with exception
pass
@@ -1386,13 +1399,15 @@ def get_available_voices(request) -> dict:
url = (base_url or f'https://{region}.tts.speech.microsoft.com') + '/cognitiveservices/voices/list'
headers = {'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY}
response = requests.get(url, headers=headers, timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
response.raise_for_status()
voices = response.json()
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(url, headers=headers) as response:
response.raise_for_status()
voices = await response.json()
for voice in voices:
available_voices[voice['ShortName']] = f'{voice["DisplayName"]} ({voice["ShortName"]})'
except requests.RequestException as e:
for voice in voices:
available_voices[voice['ShortName']] = f'{voice["DisplayName"]} ({voice["ShortName"]})'
except Exception as e:
log.error(f'Error fetching voices: {str(e)}')
elif request.app.state.config.TTS_ENGINE == 'mistral':
api_key = request.app.state.config.TTS_MISTRAL_API_KEY
@@ -1400,29 +1415,29 @@ def get_available_voices(request) -> dict:
if api_key:
try:
response = requests.get(
f'{api_base_url}/audio/voices',
headers={
'Authorization': f'Bearer {api_key}',
},
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
)
response.raise_for_status()
voices_data = response.json()
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
f'{api_base_url}/audio/voices',
headers={
'Authorization': f'Bearer {api_key}',
},
) as response:
response.raise_for_status()
voices_data = await response.json()
for voice in voices_data:
voice_id = voice.get('voice_id', voice.get('id', ''))
voice_name = voice.get('name', voice_id)
if voice_id:
available_voices[voice_id] = voice_name
except requests.RequestException as e:
for voice in voices_data:
voice_id = voice.get('voice_id', voice.get('id', ''))
voice_name = voice.get('name', voice_id)
if voice_id:
available_voices[voice_id] = voice_name
except Exception as e:
log.error(f'Error fetching Mistral voices: {str(e)}')
return available_voices
@lru_cache
def get_elevenlabs_voices(api_key: str) -> dict:
async def get_elevenlabs_voices(api_key: str) -> dict:
"""
Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs
@@ -1433,22 +1448,22 @@ def get_elevenlabs_voices(api_key: str) -> dict:
try:
# TODO: Add retries
response = requests.get(
f'{ELEVENLABS_API_BASE_URL}/v1/voices',
headers={
'xi-api-key': api_key,
'Content-Type': 'application/json',
},
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
)
response.raise_for_status()
voices_data = response.json()
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
f'{ELEVENLABS_API_BASE_URL}/v1/voices',
headers={
'xi-api-key': api_key,
'Content-Type': 'application/json',
},
) as response:
response.raise_for_status()
voices_data = await response.json()
voices = {}
for voice in voices_data.get('voices', []):
voices[voice['voice_id']] = voice['name']
except requests.RequestException as e:
# Avoid @lru_cache with exception
voices = {}
for voice in voices_data.get('voices', []):
voices[voice['voice_id']] = voice['name']
except Exception as e:
log.error(f'Error fetching voices: {str(e)}')
raise RuntimeError(f'Error fetching voices: {str(e)}')
@@ -1457,4 +1472,4 @@ def get_elevenlabs_voices(api_key: str) -> dict:
@router.get('/voices')
async def get_voices(request: Request, user=Depends(get_verified_user)):
return {'voices': [{'id': k, 'name': v} for k, v in get_available_voices(request).items()]}
return {'voices': [{'id': k, 'name': v} for k, v in (await get_available_voices(request)).items()]}