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 uuid
import html import html
import base64 import base64
from functools import lru_cache
from pydub import AudioSegment from pydub import AudioSegment
from pydub.silence import split_on_silence from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor 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': elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
voice_id = payload.get('voice', '') 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( raise HTTPException(
status_code=400, status_code=400,
detail='Invalid voice id', 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 = [] available_models = []
if request.app.state.config.TTS_ENGINE == 'openai': if request.app.state.config.TTS_ENGINE == 'openai':
# Use custom endpoint if not using the official OpenAI API URL # 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'): if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'):
try: timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
response = requests.get( async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models', try:
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, async with session.get(
) f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models',
response.raise_for_status() ) as response:
data = response.json() response.raise_for_status()
available_models = data.get('models', []) data = await response.json()
except Exception as e: available_models = data.get('models', [])
log.error(f'Error fetching models from custom endpoint: {str(e)}') except Exception as e:
available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] 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: else:
available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}]
elif request.app.state.config.TTS_ENGINE == 'elevenlabs': elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
try: try:
response = requests.get( timeout = aiohttp.ClientTimeout(total=5)
f'{ELEVENLABS_API_BASE_URL}/v1/models', async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
headers={ async with session.get(
'xi-api-key': request.app.state.config.TTS_API_KEY, f'{ELEVENLABS_API_BASE_URL}/v1/models',
'Content-Type': 'application/json', headers={
}, 'xi-api-key': request.app.state.config.TTS_API_KEY,
timeout=5, 'Content-Type': 'application/json',
) },
response.raise_for_status() ) as response:
models = response.json() response.raise_for_status()
models = await response.json()
available_models = [{'name': model['name'], 'id': model['model_id']} for model in models] available_models = [{'name': model['name'], 'id': model['model_id']} for model in models]
except requests.RequestException as e: except Exception as e:
log.error(f'Error fetching voices: {str(e)}') log.error(f'Error fetching models: {str(e)}')
elif request.app.state.config.TTS_ENGINE == 'mistral': elif request.app.state.config.TTS_ENGINE == 'mistral':
available_models = [{'id': 'mistral-tts-latest'}] available_models = [{'id': 'mistral-tts-latest'}]
return available_models return available_models
@@ -1336,24 +1348,25 @@ def get_available_models(request: Request) -> list[dict]:
@router.get('/models') @router.get('/models')
async def get_models(request: Request, user=Depends(get_verified_user)): 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""" """Returns {voice_id: voice_name} dict"""
available_voices = {} available_voices = {}
if request.app.state.config.TTS_ENGINE == 'openai': if request.app.state.config.TTS_ENGINE == 'openai':
# Use custom endpoint if not using the official OpenAI API URL # 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'): if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'):
try: try:
response = requests.get( timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices', async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, async with session.get(
) f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices',
response.raise_for_status() ) as response:
data = response.json() response.raise_for_status()
voices_list = data.get('voices', []) data = await response.json()
available_voices = {voice['id']: voice['name'] for voice in voices_list} voices_list = data.get('voices', [])
available_voices = {voice['id']: voice['name'] for voice in voices_list}
except Exception as e: except Exception as e:
log.error(f'Error fetching voices from custom endpoint: {str(e)}') log.error(f'Error fetching voices from custom endpoint: {str(e)}')
available_voices = { available_voices = {
@@ -1375,7 +1388,7 @@ def get_available_voices(request) -> dict:
} }
elif request.app.state.config.TTS_ENGINE == 'elevenlabs': elif request.app.state.config.TTS_ENGINE == 'elevenlabs':
try: 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: except Exception:
# Avoided @lru_cache with exception # Avoided @lru_cache with exception
pass 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' 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} headers = {'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY}
response = requests.get(url, headers=headers, timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
response.raise_for_status() async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
voices = response.json() async with session.get(url, headers=headers) as response:
response.raise_for_status()
voices = await response.json()
for voice in voices: for voice in voices:
available_voices[voice['ShortName']] = f'{voice["DisplayName"]} ({voice["ShortName"]})' available_voices[voice['ShortName']] = f'{voice["DisplayName"]} ({voice["ShortName"]})'
except requests.RequestException as e: except Exception as e:
log.error(f'Error fetching voices: {str(e)}') log.error(f'Error fetching voices: {str(e)}')
elif request.app.state.config.TTS_ENGINE == 'mistral': elif request.app.state.config.TTS_ENGINE == 'mistral':
api_key = request.app.state.config.TTS_MISTRAL_API_KEY api_key = request.app.state.config.TTS_MISTRAL_API_KEY
@@ -1400,29 +1415,29 @@ def get_available_voices(request) -> dict:
if api_key: if api_key:
try: try:
response = requests.get( timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
f'{api_base_url}/audio/voices', async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
headers={ async with session.get(
'Authorization': f'Bearer {api_key}', f'{api_base_url}/audio/voices',
}, headers={
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, 'Authorization': f'Bearer {api_key}',
) },
response.raise_for_status() ) as response:
voices_data = response.json() response.raise_for_status()
voices_data = await response.json()
for voice in voices_data: for voice in voices_data:
voice_id = voice.get('voice_id', voice.get('id', '')) voice_id = voice.get('voice_id', voice.get('id', ''))
voice_name = voice.get('name', voice_id) voice_name = voice.get('name', voice_id)
if voice_id: if voice_id:
available_voices[voice_id] = voice_name available_voices[voice_id] = voice_name
except requests.RequestException as e: except Exception as e:
log.error(f'Error fetching Mistral voices: {str(e)}') log.error(f'Error fetching Mistral voices: {str(e)}')
return available_voices return available_voices
@lru_cache async def get_elevenlabs_voices(api_key: str) -> dict:
def get_elevenlabs_voices(api_key: str) -> dict:
""" """
Note, set the following in your .env file to use Elevenlabs: Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs AUDIO_TTS_ENGINE=elevenlabs
@@ -1433,22 +1448,22 @@ def get_elevenlabs_voices(api_key: str) -> dict:
try: try:
# TODO: Add retries # TODO: Add retries
response = requests.get( timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
f'{ELEVENLABS_API_BASE_URL}/v1/voices', async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
headers={ async with session.get(
'xi-api-key': api_key, f'{ELEVENLABS_API_BASE_URL}/v1/voices',
'Content-Type': 'application/json', headers={
}, 'xi-api-key': api_key,
timeout=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, 'Content-Type': 'application/json',
) },
response.raise_for_status() ) as response:
voices_data = response.json() response.raise_for_status()
voices_data = await response.json()
voices = {} voices = {}
for voice in voices_data.get('voices', []): for voice in voices_data.get('voices', []):
voices[voice['voice_id']] = voice['name'] voices[voice['voice_id']] = voice['name']
except requests.RequestException as e: except Exception as e:
# Avoid @lru_cache with exception
log.error(f'Error fetching voices: {str(e)}') log.error(f'Error fetching voices: {str(e)}')
raise RuntimeError(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') @router.get('/voices')
async def get_voices(request: Request, user=Depends(get_verified_user)): 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()]}