@@ -21,13 +21,18 @@ import {
2121 openaiTTS ,
2222} from "./tts.js" ;
2323
24+ const OPENAI_SPEECH_RESPONSE_FORMATS = [ "mp3" , "opus" , "wav" ] as const ;
25+
26+ type OpenAiSpeechResponseFormat = ( typeof OPENAI_SPEECH_RESPONSE_FORMATS ) [ number ] ;
27+
2428type OpenAITtsProviderConfig = {
2529 apiKey ?: string ;
2630 baseUrl : string ;
2731 model : string ;
2832 voice : string ;
2933 speed ?: number ;
3034 instructions ?: string ;
35+ responseFormat ?: OpenAiSpeechResponseFormat ;
3136} ;
3237
3338type OpenAITtsProviderOverrides = {
@@ -36,6 +41,57 @@ type OpenAITtsProviderOverrides = {
3641 speed ?: number ;
3742} ;
3843
44+ function normalizeOpenAISpeechResponseFormat (
45+ value : unknown ,
46+ ) : OpenAiSpeechResponseFormat | undefined {
47+ const next = trimToUndefined ( typeof value === "string" ? value : undefined ) ?. toLowerCase ( ) ;
48+ if ( ! next ) {
49+ return undefined ;
50+ }
51+ if (
52+ OPENAI_SPEECH_RESPONSE_FORMATS . includes ( next as ( typeof OPENAI_SPEECH_RESPONSE_FORMATS ) [ number ] )
53+ ) {
54+ return next as OpenAiSpeechResponseFormat ;
55+ }
56+ throw new Error ( `Invalid OpenAI speech responseFormat: ${ next } ` ) ;
57+ }
58+
59+ function isGroqSpeechBaseUrl ( baseUrl : string ) : boolean {
60+ try {
61+ const hostname = new URL ( baseUrl ) . hostname . toLowerCase ( ) ;
62+ return hostname === "groq.com" || hostname . endsWith ( ".groq.com" ) ;
63+ } catch {
64+ return false ;
65+ }
66+ }
67+
68+ function resolveSpeechResponseFormat (
69+ baseUrl : string ,
70+ target : "audio-file" | "voice-note" ,
71+ configuredFormat ?: OpenAiSpeechResponseFormat ,
72+ ) : OpenAiSpeechResponseFormat {
73+ if ( configuredFormat ) {
74+ return configuredFormat ;
75+ }
76+ if ( isGroqSpeechBaseUrl ( baseUrl ) ) {
77+ return "wav" ;
78+ }
79+ return target === "voice-note" ? "opus" : "mp3" ;
80+ }
81+
82+ function responseFormatToFileExtension (
83+ format : OpenAiSpeechResponseFormat ,
84+ ) : ".mp3" | ".opus" | ".wav" {
85+ switch ( format ) {
86+ case "opus" :
87+ return ".opus" ;
88+ case "wav" :
89+ return ".wav" ;
90+ default :
91+ return ".mp3" ;
92+ }
93+ }
94+
3995function normalizeOpenAIProviderConfig (
4096 rawConfig : Record < string , unknown > ,
4197) : OpenAITtsProviderConfig {
@@ -54,6 +110,7 @@ function normalizeOpenAIProviderConfig(
54110 voice : trimToUndefined ( raw ?. voice ) ?? "coral" ,
55111 speed : asFiniteNumber ( raw ?. speed ) ,
56112 instructions : trimToUndefined ( raw ?. instructions ) ,
113+ responseFormat : normalizeOpenAISpeechResponseFormat ( raw ?. responseFormat ) ,
57114 } ;
58115}
59116
@@ -66,6 +123,8 @@ function readOpenAIProviderConfig(config: SpeechProviderConfig): OpenAITtsProvid
66123 voice : trimToUndefined ( config . voice ) ?? normalized . voice ,
67124 speed : asFiniteNumber ( config . speed ) ?? normalized . speed ,
68125 instructions : trimToUndefined ( config . instructions ) ?? normalized . instructions ,
126+ responseFormat :
127+ normalizeOpenAISpeechResponseFormat ( config . responseFormat ) ?? normalized . responseFormat ,
69128 } ;
70129}
71130
@@ -171,7 +230,11 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
171230 if ( ! apiKey ) {
172231 throw new Error ( "OpenAI API key missing" ) ;
173232 }
174- const responseFormat = req . target === "voice-note" ? "opus" : "mp3" ;
233+ const responseFormat = resolveSpeechResponseFormat (
234+ config . baseUrl ,
235+ req . target ,
236+ config . responseFormat ,
237+ ) ;
175238 const audioBuffer = await openaiTTS ( {
176239 text : req . text ,
177240 apiKey,
@@ -186,8 +249,8 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
186249 return {
187250 audioBuffer,
188251 outputFormat : responseFormat ,
189- fileExtension : responseFormat === "opus" ? ".opus" : ".mp3" ,
190- voiceCompatible : req . target === "voice-note" ,
252+ fileExtension : responseFormatToFileExtension ( responseFormat ) ,
253+ voiceCompatible : req . target === "voice-note" && responseFormat === "opus" ,
191254 } ;
192255 } ,
193256 synthesizeTelephony : async ( req ) => {
0 commit comments