@@ -168,6 +168,8 @@ class TTSConfigForm(BaseModel):
168168 AZURE_SPEECH_REGION : str
169169 AZURE_SPEECH_BASE_URL : str
170170 AZURE_SPEECH_OUTPUT_FORMAT : str
171+ MISTRAL_API_KEY : str
172+ MISTRAL_API_BASE_URL : str
171173
172174
173175class STTConfigForm (BaseModel ):
@@ -208,6 +210,8 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
208210 'AZURE_SPEECH_REGION' : request .app .state .config .TTS_AZURE_SPEECH_REGION ,
209211 'AZURE_SPEECH_BASE_URL' : request .app .state .config .TTS_AZURE_SPEECH_BASE_URL ,
210212 'AZURE_SPEECH_OUTPUT_FORMAT' : request .app .state .config .TTS_AZURE_SPEECH_OUTPUT_FORMAT ,
213+ 'MISTRAL_API_KEY' : request .app .state .config .TTS_MISTRAL_API_KEY ,
214+ 'MISTRAL_API_BASE_URL' : request .app .state .config .TTS_MISTRAL_API_BASE_URL ,
211215 },
212216 'stt' : {
213217 'OPENAI_API_BASE_URL' : request .app .state .config .STT_OPENAI_API_BASE_URL ,
@@ -242,6 +246,8 @@ async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm
242246 request .app .state .config .TTS_AZURE_SPEECH_REGION = form_data .tts .AZURE_SPEECH_REGION
243247 request .app .state .config .TTS_AZURE_SPEECH_BASE_URL = form_data .tts .AZURE_SPEECH_BASE_URL
244248 request .app .state .config .TTS_AZURE_SPEECH_OUTPUT_FORMAT = form_data .tts .AZURE_SPEECH_OUTPUT_FORMAT
249+ request .app .state .config .TTS_MISTRAL_API_KEY = form_data .tts .MISTRAL_API_KEY
250+ request .app .state .config .TTS_MISTRAL_API_BASE_URL = form_data .tts .MISTRAL_API_BASE_URL
245251
246252 request .app .state .config .STT_OPENAI_API_BASE_URL = form_data .stt .OPENAI_API_BASE_URL
247253 request .app .state .config .STT_OPENAI_API_KEY = form_data .stt .OPENAI_API_KEY
@@ -280,6 +286,8 @@ async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm
280286 'AZURE_SPEECH_REGION' : request .app .state .config .TTS_AZURE_SPEECH_REGION ,
281287 'AZURE_SPEECH_BASE_URL' : request .app .state .config .TTS_AZURE_SPEECH_BASE_URL ,
282288 'AZURE_SPEECH_OUTPUT_FORMAT' : request .app .state .config .TTS_AZURE_SPEECH_OUTPUT_FORMAT ,
289+ 'MISTRAL_API_KEY' : request .app .state .config .TTS_MISTRAL_API_KEY ,
290+ 'MISTRAL_API_BASE_URL' : request .app .state .config .TTS_MISTRAL_API_BASE_URL ,
283291 },
284292 'stt' : {
285293 'OPENAI_API_BASE_URL' : request .app .state .config .STT_OPENAI_API_BASE_URL ,
@@ -551,6 +559,76 @@ async def speech(request: Request, user=Depends(get_verified_user)):
551559
552560 return FileResponse (file_path )
553561
562+ elif request .app .state .config .TTS_ENGINE == 'mistral' :
563+ api_key = request .app .state .config .TTS_MISTRAL_API_KEY
564+ api_base_url = request .app .state .config .TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
565+
566+ if not api_key :
567+ raise HTTPException (
568+ status_code = 400 ,
569+ detail = 'Mistral API key is required for Mistral TTS' ,
570+ )
571+
572+ try :
573+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT )
574+ async with aiohttp .ClientSession (timeout = timeout , trust_env = True ) as session :
575+ mistral_payload = {
576+ 'input' : payload .get ('input' , '' ),
577+ 'model' : request .app .state .config .TTS_MODEL or 'mistral-tts-latest' ,
578+ 'voice_id' : payload .get ('voice' , '' ),
579+ 'response_format' : 'mp3' ,
580+ }
581+
582+ r = await session .post (
583+ url = f'{ api_base_url } /audio/speech' ,
584+ json = mistral_payload ,
585+ headers = {
586+ 'Content-Type' : 'application/json' ,
587+ 'Authorization' : f'Bearer { api_key } ' ,
588+ },
589+ ssl = AIOHTTP_CLIENT_SESSION_SSL ,
590+ )
591+
592+ r .raise_for_status ()
593+
594+ res = await r .json ()
595+ audio_data = res .get ('audio_data' , '' )
596+ if not audio_data :
597+ raise ValueError ('No audio_data in Mistral TTS response' )
598+
599+ audio_bytes = base64 .b64decode (audio_data )
600+
601+ async with aiofiles .open (file_path , 'wb' ) as f :
602+ await f .write (audio_bytes )
603+
604+ async with aiofiles .open (file_body_path , 'w' ) as f :
605+ await f .write (json .dumps (payload ))
606+
607+ return FileResponse (file_path )
608+
609+ except Exception as e :
610+ log .exception (e )
611+ detail = None
612+
613+ status_code = 500
614+ detail = 'Open WebUI: Server Connection Error'
615+
616+ if r is not None :
617+ status_code = r .status
618+
619+ try :
620+ res = await r .json ()
621+ if 'error' in res :
622+ detail = f'External: { res ["error" ]} '
623+ elif 'message' in res :
624+ detail = f'External: { res ["message" ]} '
625+ except Exception :
626+ detail = f'External: { e } '
627+
628+ raise HTTPException (
629+ status_code = status_code ,
630+ detail = detail ,
631+ )
554632
555633def transcription_handler (request , file_path , metadata , user = None ):
556634 filename = os .path .basename (file_path )
@@ -1238,6 +1316,8 @@ def get_available_models(request: Request) -> list[dict]:
12381316 available_models = [{'name' : model ['name' ], 'id' : model ['model_id' ]} for model in models ]
12391317 except requests .RequestException as e :
12401318 log .error (f'Error fetching voices: { str (e )} ' )
1319+ elif request .app .state .config .TTS_ENGINE == 'mistral' :
1320+ available_models = [{'id' : 'mistral-tts-latest' }]
12411321 return available_models
12421322
12431323
@@ -1301,6 +1381,29 @@ def get_available_voices(request) -> dict:
13011381 available_voices [voice ['ShortName' ]] = f'{ voice ["DisplayName" ]} ({ voice ["ShortName" ]} )'
13021382 except requests .RequestException as e :
13031383 log .error (f'Error fetching voices: { str (e )} ' )
1384+ elif request .app .state .config .TTS_ENGINE == 'mistral' :
1385+ api_key = request .app .state .config .TTS_MISTRAL_API_KEY
1386+ api_base_url = request .app .state .config .TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
1387+
1388+ if api_key :
1389+ try :
1390+ response = requests .get (
1391+ f'{ api_base_url } /audio/voices' ,
1392+ headers = {
1393+ 'Authorization' : f'Bearer { api_key } ' ,
1394+ },
1395+ timeout = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST ,
1396+ )
1397+ response .raise_for_status ()
1398+ voices_data = response .json ()
1399+
1400+ for voice in voices_data :
1401+ voice_id = voice .get ('voice_id' , voice .get ('id' , '' ))
1402+ voice_name = voice .get ('name' , voice_id )
1403+ if voice_id :
1404+ available_voices [voice_id ] = voice_name
1405+ except requests .RequestException as e :
1406+ log .error (f'Error fetching Mistral voices: { str (e )} ' )
13041407
13051408 return available_voices
13061409
0 commit comments