Feat : tts
This commit is contained in:
@@ -212,6 +212,8 @@ export const PROVIDER_MODELS = {
|
||||
// Gemini 2.0 series (retiring June 1, 2026)
|
||||
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
|
||||
{ id: "gemini-2.0-flash-lite", name: "Gemini 2.0 Flash Lite" },
|
||||
{ id: "gemma-4-31b-it", name: "Gemma 4 31B IT" },
|
||||
|
||||
// Embedding models
|
||||
{ id: "gemini-embedding-2-preview", name: "Gemini Embedding 2 Preview", type: "embedding" },
|
||||
{ id: "gemini-embedding-001", name: "Gemini Embedding 001", type: "embedding" },
|
||||
@@ -364,6 +366,63 @@ export const PROVIDER_MODELS = {
|
||||
{ id: "qwen/qwen3-next-80b-a3b-instruct-maas", name: "Qwen3 Next 80B Instruct (Vertex)" },
|
||||
{ id: "zai-org/glm-5-maas", name: "GLM-5 (Vertex)" },
|
||||
],
|
||||
|
||||
// Free/noAuth TTS providers
|
||||
"local-device": [
|
||||
{ id: "default", name: "System Default Voice", type: "tts" },
|
||||
],
|
||||
"google-tts": [
|
||||
{ id: "en", name: "English", type: "tts" },
|
||||
{ id: "vi", name: "Vietnamese", type: "tts" },
|
||||
{ id: "zh-CN", name: "Chinese (Simplified)", type: "tts" },
|
||||
{ id: "fr", name: "French", type: "tts" },
|
||||
{ id: "de", name: "German", type: "tts" },
|
||||
{ id: "ja", name: "Japanese", type: "tts" },
|
||||
{ id: "ko", name: "Korean", type: "tts" },
|
||||
],
|
||||
// OpenAI TTS voices (hardcoded — no public API to list them)
|
||||
// Used by ttsCore.js when provider = openai
|
||||
"openai-tts-voices": [
|
||||
{ id: "alloy", name: "Alloy", type: "tts" },
|
||||
{ id: "ash", name: "Ash", type: "tts" },
|
||||
{ id: "ballad", name: "Ballad", type: "tts" },
|
||||
{ id: "cedar", name: "Cedar", type: "tts" },
|
||||
{ id: "coral", name: "Coral", type: "tts" },
|
||||
{ id: "echo", name: "Echo", type: "tts" },
|
||||
{ id: "fable", name: "Fable", type: "tts" },
|
||||
{ id: "marin", name: "Marin", type: "tts" },
|
||||
{ id: "nova", name: "Nova", type: "tts" },
|
||||
{ id: "onyx", name: "Onyx", type: "tts" },
|
||||
{ id: "sage", name: "Sage", type: "tts" },
|
||||
{ id: "shimmer", name: "Shimmer", type: "tts" },
|
||||
{ id: "verse", name: "Verse", type: "tts" },
|
||||
],
|
||||
// OpenAI TTS models
|
||||
"openai-tts-models": [
|
||||
{ id: "gpt-4o-mini-tts", name: "GPT-4o Mini TTS", type: "tts" },
|
||||
{ id: "tts-1-hd", name: "TTS-1 HD", type: "tts" },
|
||||
{ id: "tts-1", name: "TTS-1", type: "tts" },
|
||||
],
|
||||
// ElevenLabs TTS models
|
||||
"elevenlabs-tts-models": [
|
||||
{ id: "eleven_flash_v2_5", name: "Flash v2.5 (Fastest)", type: "tts" },
|
||||
{ id: "eleven_turbo_v2_5", name: "Turbo v2.5 (Fast)", type: "tts" },
|
||||
{ id: "eleven_multilingual_v2", name: "Multilingual v2 (Quality)", type: "tts" },
|
||||
{ id: "eleven_monolingual_v1", name: "Monolingual v1 (English)", type: "tts" },
|
||||
],
|
||||
"edge-tts": [
|
||||
{ id: "en-US-AriaNeural", name: "Aria (en-US)", type: "tts" },
|
||||
{ id: "en-US-GuyNeural", name: "Guy (en-US)", type: "tts" },
|
||||
{ id: "en-GB-SoniaNeural", name: "Sonia (en-GB)", type: "tts" },
|
||||
{ id: "vi-VN-HoaiMyNeural", name: "Hoai My (vi-VN)", type: "tts" },
|
||||
{ id: "vi-VN-NamMinhNeural", name: "Nam Minh (vi-VN)", type: "tts" },
|
||||
{ id: "zh-CN-XiaoxiaoNeural", name: "Xiaoxiao (zh-CN)", type: "tts" },
|
||||
{ id: "zh-CN-YunxiNeural", name: "Yunxi (zh-CN)", type: "tts" },
|
||||
{ id: "fr-FR-DeniseNeural", name: "Denise (fr-FR)", type: "tts" },
|
||||
{ id: "de-DE-KatjaNeural", name: "Katja (de-DE)", type: "tts" },
|
||||
{ id: "ja-JP-NanamiNeural", name: "Nanami (ja-JP)", type: "tts" },
|
||||
{ id: "ko-KR-SunHiNeural", name: "SunHi (ko-KR)", type: "tts" },
|
||||
],
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
|
||||
419
open-sse/handlers/ttsCore.js
Normal file
419
open-sse/handlers/ttsCore.js
Normal file
@@ -0,0 +1,419 @@
|
||||
import { createErrorResult } from "../utils/error.js";
|
||||
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { mkdtemp, readFile, rm } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// ── Response Formatter (DRY) ───────────────────────────────────
|
||||
function createTtsResponse(base64Audio, format, responseFormat) {
|
||||
const audioBuffer = Buffer.from(base64Audio, "base64");
|
||||
|
||||
// JSON format: return base64 encoded audio
|
||||
if (responseFormat === "json") {
|
||||
return {
|
||||
success: true,
|
||||
response: new Response(JSON.stringify({ audio: base64Audio, format }), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Binary format (default): return raw MP3
|
||||
return {
|
||||
success: true,
|
||||
response: new Response(audioBuffer, {
|
||||
headers: {
|
||||
"Content-Type": `audio/${format}`,
|
||||
"Content-Length": String(audioBuffer.length),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Token cache per engine ─────────────────────────────────────
|
||||
const cache = {
|
||||
google: { token: null, tokenTime: 0 },
|
||||
bing: { token: null, tokenTime: 0 },
|
||||
};
|
||||
|
||||
const GOOGLE_REFRESH = 11 * 60 * 1000;
|
||||
const BING_REFRESH = 5 * 60 * 1000; // conservative: token TTL is 1h but refresh early
|
||||
|
||||
const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
||||
const SEC_CH_HEADERS = {
|
||||
"sec-ch-ua": '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
|
||||
"sec-ch-ua-arch": '"arm"',
|
||||
"sec-ch-ua-bitness": '"64"',
|
||||
"sec-ch-ua-full-version": '"146.0.7680.178"',
|
||||
"sec-ch-ua-full-version-list": '"Chromium";v="146.0.7680.178", "Not-A.Brand";v="24.0.0.0", "Google Chrome";v="146.0.7680.178"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-model": '""',
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-ch-ua-platform-version": '"15.1.0"',
|
||||
};
|
||||
|
||||
// ── Google TTS ─────────────────────────────────────────────────
|
||||
async function getGoogleToken() {
|
||||
const now = Date.now();
|
||||
if (cache.google.token && now - cache.google.tokenTime < GOOGLE_REFRESH) {
|
||||
return cache.google.token;
|
||||
}
|
||||
const res = await fetch("https://translate.google.com/", {
|
||||
headers: { "User-Agent": UA },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Google translate fetch failed: ${res.status}`);
|
||||
const html = await res.text();
|
||||
const fSid = html.match(/"FdrFJe":"(.*?)"/)?.[ 1];
|
||||
const bl = html.match(/"cfb2h":"(.*?)"/)?.[ 1];
|
||||
if (!fSid || !bl) throw new Error("Failed to parse Google token");
|
||||
cache.google.token = { "f.sid": fSid, bl };
|
||||
cache.google.tokenTime = now;
|
||||
return cache.google.token;
|
||||
}
|
||||
|
||||
let _googleIdx = 0;
|
||||
async function googleTts(text, lang) {
|
||||
const token = await getGoogleToken();
|
||||
const cleanText = text.replace(/[@^*()\\/\-_+=><"'\u201c\u201d\u3010\u3011]/g, " ").replaceAll(", ", ". ");
|
||||
const rpcId = "jQ1olc";
|
||||
const reqId = (++_googleIdx * 100000) + Math.floor(1000 + Math.random() * 9000);
|
||||
const query = new URLSearchParams({
|
||||
rpcids: rpcId,
|
||||
"f.sid": token["f.sid"],
|
||||
bl: token.bl,
|
||||
hl: lang,
|
||||
"soc-app": 1, "soc-platform": 1, "soc-device": 1,
|
||||
_reqid: reqId,
|
||||
rt: "c",
|
||||
});
|
||||
const payload = [cleanText, lang, null, "undefined", [0]];
|
||||
const body = new URLSearchParams();
|
||||
body.append("f.req", JSON.stringify([[[rpcId, JSON.stringify(payload), null, "generic"]]]));
|
||||
const res = await fetch(`https://translate.google.com/_/TranslateWebserverUi/data/batchexecute?${query}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded", "Referer": "https://translate.google.com/" },
|
||||
body: body.toString(),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Google TTS failed: ${res.status}`);
|
||||
const data = await res.text();
|
||||
const split = JSON.parse(data.split("\n")[3]);
|
||||
const base64 = JSON.parse(split[0][2])[0];
|
||||
if (!base64 || base64.length < 100) throw new Error("Google TTS returned empty audio");
|
||||
return base64; // base64 MP3
|
||||
}
|
||||
|
||||
// ── Bing TTS ───────────────────────────────────────────────────
|
||||
async function getBingToken() {
|
||||
const now = Date.now();
|
||||
if (cache.bing.token && now - cache.bing.tokenTime < BING_REFRESH) {
|
||||
return cache.bing.token;
|
||||
}
|
||||
const res = await fetch("https://www.bing.com/translator", {
|
||||
headers: { "User-Agent": UA, "Accept-Language": "vi,en-US;q=0.9,en;q=0.8" },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Bing translator fetch failed: ${res.status}`);
|
||||
const rawCookies = res.headers.getSetCookie?.() || [];
|
||||
const cookie = rawCookies.map((c) => c.split(";")[0]).join("; ");
|
||||
const html = await res.text();
|
||||
const match = html.match(/params_AbusePreventionHelper\s*=\s*\[([^,]+),([^,]+),/);
|
||||
if (!match) throw new Error("Failed to parse Bing token");
|
||||
cache.bing.token = { key: match[1], token: match[2].replace(/"/g, ""), cookie };
|
||||
cache.bing.tokenTime = now;
|
||||
return cache.bing.token;
|
||||
}
|
||||
|
||||
async function bingTtsRequest(text, voiceId, token) {
|
||||
const parts = voiceId.split("-");
|
||||
const xmlLang = parts.slice(0, 2).join("-");
|
||||
const gender = voiceId.toLowerCase().includes("male") ? "Male" : "Female";
|
||||
const ssml = `<speak version='1.0' xml:lang='${xmlLang}'><voice xml:lang='${xmlLang}' xml:gender='${gender}' name='${voiceId}'><prosody rate='0.00%'>${text}</prosody></voice></speak>`;
|
||||
const body = new URLSearchParams();
|
||||
body.append("ssml", ssml);
|
||||
body.append("token", token.token);
|
||||
body.append("key", token.key);
|
||||
return fetch("https://www.bing.com/tfettts?isVertical=1&&IG=1&IID=translator.5023&SFX=1", {
|
||||
method: "POST",
|
||||
body: body.toString(),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "*/*",
|
||||
"Origin": "https://www.bing.com",
|
||||
"Referer": "https://www.bing.com/translator",
|
||||
"User-Agent": UA,
|
||||
...(token.cookie ? { "Cookie": token.cookie } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function bingTts(text, voiceId) {
|
||||
let token = await getBingToken();
|
||||
let res = await bingTtsRequest(text, voiceId, token);
|
||||
|
||||
// On 429/captcha: invalidate cache and retry once with fresh token
|
||||
if (res.status === 429 || res.status === 403) {
|
||||
cache.bing.token = null;
|
||||
cache.bing.tokenTime = 0;
|
||||
token = await getBingToken();
|
||||
res = await bingTtsRequest(text, voiceId, token);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => "");
|
||||
throw new Error(`Bing TTS failed: ${res.status}${body ? " - " + body : ""}`);
|
||||
}
|
||||
const buf = await res.arrayBuffer();
|
||||
if (buf.byteLength < 1024) throw new Error("Bing TTS returned empty audio");
|
||||
return Buffer.from(buf).toString("base64"); // base64 MP3
|
||||
}
|
||||
|
||||
// ── Local Device TTS (macOS `say` + ffmpeg) ───────────────────
|
||||
let _localVoicesCache = null;
|
||||
|
||||
export async function fetchLocalDeviceVoices() {
|
||||
if (_localVoicesCache) return _localVoicesCache;
|
||||
try {
|
||||
const { stdout } = await execFileAsync("say", ["-v", "?"]);
|
||||
const voices = [];
|
||||
for (const line of stdout.split("\n")) {
|
||||
// Format: "Name locale # sample"
|
||||
const m = line.match(/^([^\s].*?)\s{2,}([a-z]{2}_[A-Z]{2})/);
|
||||
if (!m) continue;
|
||||
const name = m[1].trim();
|
||||
const locale = m[2].trim(); // e.g. en_US
|
||||
const lang = locale.split("_")[0];
|
||||
const country = locale.split("_")[1];
|
||||
voices.push({ id: name, name, locale, lang, country, gender: "" });
|
||||
}
|
||||
_localVoicesCache = voices;
|
||||
return voices;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function localDeviceTts(text, voiceId) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "tts-"));
|
||||
const aiffPath = join(dir, "out.aiff");
|
||||
const mp3Path = join(dir, "out.mp3");
|
||||
try {
|
||||
const args = voiceId ? ["-v", voiceId, "-o", aiffPath, text] : ["-o", aiffPath, text];
|
||||
await execFileAsync("say", args);
|
||||
await execFileAsync("ffmpeg", ["-y", "-i", aiffPath, "-codec:a", "libmp3lame", "-qscale:a", "4", mp3Path]);
|
||||
const buf = await readFile(mp3Path);
|
||||
return buf.toString("base64");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Voices list (Edge TTS public endpoint) ─────────────────────
|
||||
let _voicesCache = null;
|
||||
let _voicesCacheTime = 0;
|
||||
const VOICES_TTL = 24 * 60 * 60 * 1000;
|
||||
|
||||
export async function fetchEdgeTtsVoices() {
|
||||
const now = Date.now();
|
||||
if (_voicesCache && now - _voicesCacheTime < VOICES_TTL) return _voicesCache;
|
||||
const res = await fetch(
|
||||
"https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=6A5AA1D4EAFF4E9FB37E23D68491D6F4",
|
||||
{ headers: { "User-Agent": UA } }
|
||||
);
|
||||
if (!res.ok) throw new Error(`Edge TTS voices fetch failed: ${res.status}`);
|
||||
const voices = await res.json();
|
||||
_voicesCache = voices;
|
||||
_voicesCacheTime = now;
|
||||
return voices;
|
||||
}
|
||||
|
||||
// ── ElevenLabs TTS ─────────────────────────────────────────────
|
||||
const _elevenlabsVoicesCache = new Map(); // Cache by API key
|
||||
|
||||
export async function fetchElevenLabsVoices(apiKey) {
|
||||
if (!apiKey) throw new Error("ElevenLabs API key required");
|
||||
|
||||
const now = Date.now();
|
||||
const cached = _elevenlabsVoicesCache.get(apiKey);
|
||||
if (cached && now - cached.time < VOICES_TTL) {
|
||||
return cached.voices;
|
||||
}
|
||||
|
||||
const res = await fetch("https://api.elevenlabs.io/v1/voices", {
|
||||
headers: {
|
||||
"xi-api-key": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`ElevenLabs voices fetch failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
// Normalize: add lang from labels.language for grouping
|
||||
const voices = (data.voices || []).map((v) => ({
|
||||
...v,
|
||||
lang: v.labels?.language || "en",
|
||||
}));
|
||||
_elevenlabsVoicesCache.set(apiKey, { voices, time: now });
|
||||
return voices;
|
||||
}
|
||||
|
||||
async function elevenlabsTts(text, voiceId, apiKey, modelId = "eleven_flash_v2_5") {
|
||||
const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"xi-api-key": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
model_id: modelId,
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.75,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err?.detail?.message || `ElevenLabs TTS failed: ${res.status}`);
|
||||
}
|
||||
|
||||
const buf = await res.arrayBuffer();
|
||||
if (buf.byteLength < 1024) throw new Error("ElevenLabs TTS returned empty audio");
|
||||
return Buffer.from(buf).toString("base64");
|
||||
}
|
||||
|
||||
// ── Voice Fetcher Registry (DRY) ───────────────────────────────
|
||||
export const VOICE_FETCHERS = {
|
||||
"edge-tts": fetchEdgeTtsVoices,
|
||||
"local-device": fetchLocalDeviceVoices,
|
||||
"elevenlabs": fetchElevenLabsVoices,
|
||||
// google-tts: uses hardcoded language codes
|
||||
// openai: uses hardcoded voices from providerModels.js
|
||||
};
|
||||
|
||||
// ── OpenAI TTS ───────────────────────────────────────────────────────────────
|
||||
async function handleOpenAiTts({ model, input, credentials, responseFormat = "mp3" }) {
|
||||
if (!credentials?.apiKey) {
|
||||
return createErrorResult(HTTP_STATUS.UNAUTHORIZED, "No OpenAI API key configured");
|
||||
}
|
||||
|
||||
// model format: "tts-model/voice" e.g. "tts-1/alloy" or "gpt-4o-mini-tts/nova"
|
||||
let ttsModel = "gpt-4o-mini-tts";
|
||||
let voice = "alloy";
|
||||
if (model && model.includes("/")) {
|
||||
const parts = model.split("/");
|
||||
if (parts.length === 2) {
|
||||
[ttsModel, voice] = parts;
|
||||
}
|
||||
} else if (model) {
|
||||
voice = model;
|
||||
}
|
||||
|
||||
const baseUrl = (credentials.baseUrl || "https://api.openai.com").replace(/\/+$/, "");
|
||||
const res = await fetch(`${baseUrl}/v1/audio/speech`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${credentials.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({ model: ttsModel, voice, input }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return createErrorResult(res.status, err?.error?.message || `OpenAI TTS failed: ${res.status}`);
|
||||
}
|
||||
|
||||
const buf = await res.arrayBuffer();
|
||||
const base64 = Buffer.from(buf).toString("base64");
|
||||
return createTtsResponse(base64, "mp3", responseFormat);
|
||||
}
|
||||
|
||||
// ── TTS Provider Registry (DRY) ────────────────────────────────
|
||||
const TTS_PROVIDERS = {
|
||||
"google-tts": {
|
||||
synthesize: async (text, model) => {
|
||||
const base64 = await googleTts(text, model || "en");
|
||||
return { base64, format: "mp3" };
|
||||
},
|
||||
requiresCredentials: false,
|
||||
},
|
||||
"edge-tts": {
|
||||
synthesize: async (text, model) => {
|
||||
const base64 = await bingTts(text, model || "vi-VN-HoaiMyNeural");
|
||||
return { base64, format: "mp3" };
|
||||
},
|
||||
requiresCredentials: false,
|
||||
},
|
||||
"local-device": {
|
||||
synthesize: async (text, model) => {
|
||||
const base64 = await localDeviceTts(text, model);
|
||||
return { base64, format: "mp3" };
|
||||
},
|
||||
requiresCredentials: false,
|
||||
},
|
||||
"elevenlabs": {
|
||||
synthesize: async (text, model, credentials) => {
|
||||
if (!credentials?.apiKey) {
|
||||
throw new Error("ElevenLabs API key required");
|
||||
}
|
||||
// model format: "voice_id" or "model_id/voice_id"
|
||||
let modelId = "eleven_flash_v2_5";
|
||||
let voiceId = model;
|
||||
if (model && model.includes("/")) {
|
||||
[modelId, voiceId] = model.split("/");
|
||||
}
|
||||
const base64 = await elevenlabsTts(text, voiceId, credentials.apiKey, modelId);
|
||||
return { base64, format: "mp3" };
|
||||
},
|
||||
requiresCredentials: true,
|
||||
},
|
||||
"openai": {
|
||||
synthesize: async (text, model, credentials, responseFormat) => {
|
||||
return await handleOpenAiTts({ model, input: text, credentials, responseFormat });
|
||||
},
|
||||
requiresCredentials: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Core handler ───────────────────────────────────────────────
|
||||
/**
|
||||
* Synthesize text to audio.
|
||||
* @param {object} options
|
||||
* @param {string} options.provider - "google-tts" | "edge-tts" | "local-device" | "openai"
|
||||
* @param {string} options.model - voice/lang id
|
||||
* @param {string} options.input - text to synthesize
|
||||
* @param {object} [options.credentials] - required for openai
|
||||
* @param {string} [options.responseFormat] - "mp3" (default) | "json" (base64)
|
||||
* @returns {Promise<{success, response, status?, error?}>}
|
||||
*/
|
||||
export async function handleTtsCore({ provider, model, input, credentials, responseFormat = "mp3" }) {
|
||||
if (!input?.trim()) {
|
||||
return createErrorResult(HTTP_STATUS.BAD_REQUEST, "Missing required field: input");
|
||||
}
|
||||
|
||||
const ttsProvider = TTS_PROVIDERS[provider];
|
||||
if (!ttsProvider) {
|
||||
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Provider '${provider}' does not support TTS via this route.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await ttsProvider.synthesize(input.trim(), model, credentials, responseFormat);
|
||||
|
||||
// OpenAI returns full response object
|
||||
if (result.success !== undefined) return result;
|
||||
|
||||
// Other providers return { base64, format }
|
||||
return createTtsResponse(result.base64, result.format, responseFormat);
|
||||
} catch (err) {
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, err.message || "TTS synthesis failed");
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const ALIAS_TO_PROVIDER_ID = {
|
||||
kmc: "kimi-coding",
|
||||
cl: "cline",
|
||||
oc: "opencode",
|
||||
// TTS providers
|
||||
el: "elevenlabs",
|
||||
// API Key providers
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
|
||||
Reference in New Issue
Block a user