55import uuid
66import html
77import base64
8- from functools import lru_cache
98from pydub import AudioSegment
109from pydub .silence import split_on_silence
1110from concurrent .futures import ThreadPoolExecutor
@@ -421,7 +420,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
421420 elif request .app .state .config .TTS_ENGINE == 'elevenlabs' :
422421 voice_id = payload .get ('voice' , '' )
423422
424- if voice_id not in get_available_voices (request ):
423+ if voice_id not in await get_available_voices (request ):
425424 raise HTTPException (
426425 status_code = 400 ,
427426 detail = 'Invalid voice id' ,
@@ -1295,65 +1294,79 @@ async def transcription(
12951294 )
12961295
12971296
1298- def get_available_models (request : Request ) -> list [dict ]:
1297+ async def get_available_models (request : Request ) -> list [dict ]:
12991298 available_models = []
13001299 if request .app .state .config .TTS_ENGINE == 'openai' :
13011300 # Use custom endpoint if not using the official OpenAI API URL
13021301 if not request .app .state .config .TTS_OPENAI_API_BASE_URL .startswith ('https://api.openai.com' ):
1303- try :
1304- response = requests .get (
1305- f'{ request .app .state .config .TTS_OPENAI_API_BASE_URL } /audio/models' ,
1306- timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST ,
1307- )
1308- response .raise_for_status ()
1309- data = response .json ()
1310- available_models = data .get ('models' , [])
1311- except Exception as e :
1312- log .error (f'Error fetching models from custom endpoint: { str (e )} ' )
1313- available_models = [{'id' : 'tts-1' }, {'id' : 'tts-1-hd' }]
1302+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1303+ async with aiohttp .ClientSession (timeout = timeout , trust_env = True ) as session :
1304+ try :
1305+ async with session .get (
1306+ f'{ request .app .state .config .TTS_OPENAI_API_BASE_URL } /audio/models' ,
1307+ ) as response :
1308+ response .raise_for_status ()
1309+ data = await response .json ()
1310+ available_models = data .get ('models' , [])
1311+ except Exception as e :
1312+ log .debug (f'/audio/models not available, trying /models fallback: { str (e )} ' )
1313+ # Fallback to standard OpenAI-compatible /models endpoint
1314+ # (used by KokoroTTS and similar custom TTS servers)
1315+ try :
1316+ async with session .get (
1317+ f'{ request .app .state .config .TTS_OPENAI_API_BASE_URL } /models' ,
1318+ ) as response :
1319+ response .raise_for_status ()
1320+ data = await response .json ()
1321+ # OpenAI /models returns {"data": [...]}, /audio/models returns {"models": [...]}
1322+ available_models = data .get ('data' , data .get ('models' , []))
1323+ except Exception as e2 :
1324+ log .error (f'Error fetching models from custom endpoint: { str (e2 )} ' )
1325+ available_models = [{'id' : 'tts-1' }, {'id' : 'tts-1-hd' }]
13141326 else :
13151327 available_models = [{'id' : 'tts-1' }, {'id' : 'tts-1-hd' }]
13161328 elif request .app .state .config .TTS_ENGINE == 'elevenlabs' :
13171329 try :
1318- response = requests . get (
1319- f' { ELEVENLABS_API_BASE_URL } /v1/models' ,
1320- headers = {
1321- 'xi-api-key' : request . app . state . config . TTS_API_KEY ,
1322- 'Content-Type' : 'application/json' ,
1323- } ,
1324- timeout = 5 ,
1325- )
1326- response . raise_for_status ()
1327- models = response .json ()
1328-
1329- available_models = [{'name' : model ['name' ], 'id' : model ['model_id' ]} for model in models ]
1330- except requests . RequestException as e :
1331- log .error (f'Error fetching voices : { str (e )} ' )
1330+ timeout = aiohttp . ClientTimeout ( total = 5 )
1331+ async with aiohttp . ClientSession ( timeout = timeout , trust_env = True ) as session :
1332+ async with session . get (
1333+ f' { ELEVENLABS_API_BASE_URL } /v1/models' ,
1334+ headers = {
1335+ 'xi-api-key' : request . app . state . config . TTS_API_KEY ,
1336+ 'Content-Type' : 'application/json' ,
1337+ },
1338+ ) as response :
1339+ response .raise_for_status ()
1340+ models = await response . json ()
1341+ available_models = [{'name' : model ['name' ], 'id' : model ['model_id' ]} for model in models ]
1342+ except Exception as e :
1343+ log .error (f'Error fetching models : { str (e )} ' )
13321344 elif request .app .state .config .TTS_ENGINE == 'mistral' :
13331345 available_models = [{'id' : 'mistral-tts-latest' }]
13341346 return available_models
13351347
13361348
13371349@router .get ('/models' )
13381350async def get_models (request : Request , user = Depends (get_verified_user )):
1339- return {'models' : get_available_models (request )}
1351+ return {'models' : await get_available_models (request )}
13401352
13411353
1342- def get_available_voices (request ) -> dict :
1354+ async def get_available_voices (request ) -> dict :
13431355 """Returns {voice_id: voice_name} dict"""
13441356 available_voices = {}
13451357 if request .app .state .config .TTS_ENGINE == 'openai' :
13461358 # Use custom endpoint if not using the official OpenAI API URL
13471359 if not request .app .state .config .TTS_OPENAI_API_BASE_URL .startswith ('https://api.openai.com' ):
13481360 try :
1349- response = requests .get (
1350- f'{ request .app .state .config .TTS_OPENAI_API_BASE_URL } /audio/voices' ,
1351- timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST ,
1352- )
1353- response .raise_for_status ()
1354- data = response .json ()
1355- voices_list = data .get ('voices' , [])
1356- available_voices = {voice ['id' ]: voice ['name' ] for voice in voices_list }
1361+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1362+ async with aiohttp .ClientSession (timeout = timeout , trust_env = True ) as session :
1363+ async with session .get (
1364+ f'{ request .app .state .config .TTS_OPENAI_API_BASE_URL } /audio/voices' ,
1365+ ) as response :
1366+ response .raise_for_status ()
1367+ data = await response .json ()
1368+ voices_list = data .get ('voices' , [])
1369+ available_voices = {voice ['id' ]: voice ['name' ] for voice in voices_list }
13571370 except Exception as e :
13581371 log .error (f'Error fetching voices from custom endpoint: { str (e )} ' )
13591372 available_voices = {
@@ -1375,7 +1388,7 @@ def get_available_voices(request) -> dict:
13751388 }
13761389 elif request .app .state .config .TTS_ENGINE == 'elevenlabs' :
13771390 try :
1378- available_voices = get_elevenlabs_voices (api_key = request .app .state .config .TTS_API_KEY )
1391+ available_voices = await get_elevenlabs_voices (api_key = request .app .state .config .TTS_API_KEY )
13791392 except Exception :
13801393 # Avoided @lru_cache with exception
13811394 pass
@@ -1386,43 +1399,45 @@ def get_available_voices(request) -> dict:
13861399 url = (base_url or f'https://{ region } .tts.speech.microsoft.com' ) + '/cognitiveservices/voices/list'
13871400 headers = {'Ocp-Apim-Subscription-Key' : request .app .state .config .TTS_API_KEY }
13881401
1389- response = requests .get (url , headers = headers , timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1390- response .raise_for_status ()
1391- voices = response .json ()
1402+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1403+ async with aiohttp .ClientSession (timeout = timeout , trust_env = True ) as session :
1404+ async with session .get (url , headers = headers ) as response :
1405+ response .raise_for_status ()
1406+ voices = await response .json ()
13921407
1393- for voice in voices :
1394- available_voices [voice ['ShortName' ]] = f'{ voice ["DisplayName" ]} ({ voice ["ShortName" ]} )'
1395- except requests . RequestException as e :
1408+ for voice in voices :
1409+ available_voices [voice ['ShortName' ]] = f'{ voice ["DisplayName" ]} ({ voice ["ShortName" ]} )'
1410+ except Exception as e :
13961411 log .error (f'Error fetching voices: { str (e )} ' )
13971412 elif request .app .state .config .TTS_ENGINE == 'mistral' :
13981413 api_key = request .app .state .config .TTS_MISTRAL_API_KEY
13991414 api_base_url = request .app .state .config .TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
14001415
14011416 if api_key :
14021417 try :
1403- response = requests .get (
1404- f'{ api_base_url } /audio/voices' ,
1405- headers = {
1406- 'Authorization' : f'Bearer { api_key } ' ,
1407- },
1408- timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST ,
1409- )
1410- response .raise_for_status ()
1411- voices_data = response .json ()
1412-
1413- for voice in voices_data :
1414- voice_id = voice .get ('voice_id' , voice .get ('id' , '' ))
1415- voice_name = voice .get ('name' , voice_id )
1416- if voice_id :
1417- available_voices [voice_id ] = voice_name
1418- except requests .RequestException as e :
1418+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1419+ async with aiohttp .ClientSession (timeout = timeout , trust_env = True ) as session :
1420+ async with session .get (
1421+ f'{ api_base_url } /audio/voices' ,
1422+ headers = {
1423+ 'Authorization' : f'Bearer { api_key } ' ,
1424+ },
1425+ ) as response :
1426+ response .raise_for_status ()
1427+ voices_data = await response .json ()
1428+
1429+ for voice in voices_data :
1430+ voice_id = voice .get ('voice_id' , voice .get ('id' , '' ))
1431+ voice_name = voice .get ('name' , voice_id )
1432+ if voice_id :
1433+ available_voices [voice_id ] = voice_name
1434+ except Exception as e :
14191435 log .error (f'Error fetching Mistral voices: { str (e )} ' )
14201436
14211437 return available_voices
14221438
14231439
1424- @lru_cache
1425- def get_elevenlabs_voices (api_key : str ) -> dict :
1440+ async def get_elevenlabs_voices (api_key : str ) -> dict :
14261441 """
14271442 Note, set the following in your .env file to use Elevenlabs:
14281443 AUDIO_TTS_ENGINE=elevenlabs
@@ -1433,22 +1448,22 @@ def get_elevenlabs_voices(api_key: str) -> dict:
14331448
14341449 try :
14351450 # TODO: Add retries
1436- response = requests . get (
1437- f' { ELEVENLABS_API_BASE_URL } /v1/voices' ,
1438- headers = {
1439- 'xi-api-key' : api_key ,
1440- 'Content-Type' : 'application/json' ,
1441- } ,
1442- timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST ,
1443- )
1444- response . raise_for_status ()
1445- voices_data = response .json ()
1446-
1447- voices = {}
1448- for voice in voices_data . get ( ' voices' , []):
1449- voices [ voice [ 'voice_id' ]] = voice [ 'name' ]
1450- except requests . RequestException as e :
1451- # Avoid @lru_cache with exception
1451+ timeout = aiohttp . ClientTimeout ( total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
1452+ async with aiohttp . ClientSession ( timeout = timeout , trust_env = True ) as session :
1453+ async with session . get (
1454+ f' { ELEVENLABS_API_BASE_URL } /v1/voices' ,
1455+ headers = {
1456+ 'xi-api-key' : api_key ,
1457+ 'Content-Type' : 'application/json' ,
1458+ },
1459+ ) as response :
1460+ response .raise_for_status ()
1461+ voices_data = await response . json ()
1462+
1463+ voices = {}
1464+ for voice in voices_data . get ( 'voices' , []):
1465+ voices [ voice [ 'voice_id' ]] = voice [ 'name' ]
1466+ except Exception as e :
14521467 log .error (f'Error fetching voices: { str (e )} ' )
14531468 raise RuntimeError (f'Error fetching voices: { str (e )} ' )
14541469
@@ -1457,4 +1472,4 @@ def get_elevenlabs_voices(api_key: str) -> dict:
14571472
14581473@router .get ('/voices' )
14591474async def get_voices (request : Request , user = Depends (get_verified_user )):
1460- return {'voices' : [{'id' : k , 'name' : v } for k , v in get_available_voices (request ).items ()]}
1475+ return {'voices' : [{'id' : k , 'name' : v } for k , v in ( await get_available_voices (request ) ).items ()]}
0 commit comments