diff --git a/src/tts/tts.ts b/src/tts/tts.ts index af3d7fda5..bec548541 100644 --- a/src/tts/tts.ts +++ b/src/tts/tts.ts @@ -758,10 +758,21 @@ export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as con * When set, model/voice validation is relaxed to allow non-OpenAI models. * Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1 */ -const OPENAI_TTS_BASE_URL = ( - process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1" -).replace(/\/+$/, ""); -const isCustomOpenAIEndpoint = OPENAI_TTS_BASE_URL !== "https://api.openai.com/v1"; +const DEFAULT_OPENAI_TTS_BASE_URL = "https://api.openai.com/v1"; +/** + * Custom OpenAI-compatible TTS endpoint. + * When set, model/voice validation is relaxed to allow non-OpenAI models. + * Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1 + */ +function getOpenAITtsBaseUrl(): string { + return ( + process.env.OPENAI_TTS_BASE_URL?.trim() || DEFAULT_OPENAI_TTS_BASE_URL + ).replace(/\/+$/, ""); +} + +function isCustomOpenAIEndpoint(): boolean { + return getOpenAITtsBaseUrl() !== DEFAULT_OPENAI_TTS_BASE_URL; +} export const OPENAI_TTS_VOICES = [ "alloy", "ash", @@ -778,13 +789,13 @@ type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number]; function isValidOpenAIModel(model: string): boolean { // Allow any model when using custom endpoint (e.g., Kokoro, LocalAI) - if (isCustomOpenAIEndpoint) return true; + if (isCustomOpenAIEndpoint()) return true; return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]); } function isValidOpenAIVoice(voice: string): voice is OpenAiTtsVoice { // Allow any voice when using custom endpoint (e.g., Kokoro Chinese voices) - if (isCustomOpenAIEndpoint) return true; + if (isCustomOpenAIEndpoint()) return true; return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice); } @@ -1011,7 +1022,7 @@ async function openaiTTS(params: { const timeout = setTimeout(() => controller.abort(), timeoutMs); try { - const response = await fetch(`${OPENAI_TTS_BASE_URL}/audio/speech`, { + const response = await fetch(`${getOpenAITtsBaseUrl()}/audio/speech`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, @@ -1365,9 +1376,9 @@ export async function maybeApplyTtsToPayload(params: { visibleText === text.trim() ? params.payload : { - ...params.payload, - text: visibleText.length > 0 ? visibleText : undefined, - }; + ...params.payload, + text: visibleText.length > 0 ? visibleText : undefined, + }; if (autoMode === "tagged" && !directives.hasDirective) return nextPayload; if (autoMode === "inbound" && params.inboundAudio !== true) return nextPayload;