Files
9router/open-sse/handlers/ttsProviders/edgeTts.js
2026-05-04 11:29:02 +07:00

90 lines
3.4 KiB
JavaScript

// Microsoft Edge / Bing TTS (no auth) — via Bing translator endpoint
import { Buffer } from "node:buffer";
import { UA } from "./_base.js";
const REFRESH_MS = 5 * 60 * 1000; // token TTL ~1h, refresh early
const VOICES_TTL = 24 * 60 * 60 * 1000;
const cache = { token: null, tokenTime: 0 };
let _voicesCache = null;
let _voicesCacheTime = 0;
async function getToken() {
const now = Date.now();
if (cache.token && now - cache.tokenTime < REFRESH_MS) return cache.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.token = { key: match[1], token: match[2].replace(/"/g, ""), cookie };
cache.tokenTime = now;
return cache.token;
}
async function ttsRequest(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 } : {}),
},
});
}
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;
}
export default {
noAuth: true,
async synthesize(text, model) {
const voiceId = model || "vi-VN-HoaiMyNeural";
let token = await getToken();
let res = await ttsRequest(text, voiceId, token);
// 429/403: invalidate cache and retry once
if (res.status === 429 || res.status === 403) {
cache.token = null;
cache.tokenTime = 0;
token = await getToken();
res = await ttsRequest(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 { base64: Buffer.from(buf).toString("base64"), format: "mp3" };
},
};