@andresaya/edge-tts
Version:
Edge TTS is a package that allows access to the online text-to-speech service used by Microsoft Edge without the need for Microsoft Edge, Windows, or an API key.
388 lines (385 loc) • 14.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/config/constants.ts
var Constants = {
TRUSTED_CLIENT_TOKEN: "6A5AA1D4EAFF4E9FB37E23D68491D6F4",
BASE_URL: "https://speech.platform.bing.com/consumer/speech/synthesize/readaloud",
WSS_URL: "wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1",
VOICES_URL: "https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list",
CHROMIUM_FULL_VERSION: "143.0.3650.75",
CHROMIUM_MAJOR_VERSION: "143",
VERSION_MS_GEC: "1-143.0.3650",
token32() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
},
getBaseHeaders() {
return {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"Cookie": "MUID=" + this.token32()
};
},
WSS_HEADERS: {
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
"Sec-WebSocket-Protocol": "synthesize",
"Sec-WebSocket-Version": "13",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.3650.75 Safari/537.36 Edg/143.0.3650.75"
},
VOICE_HEADERS: {
"Sec-CH-UA": '" Not;A Brand";v="99", "Microsoft Edge";v="143", "Chromium";v="143"',
"Sec-CH-UA-Mobile": "?0",
"Accept": "*/*",
"Sec-Fetch-Site": "none",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty"
},
// https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=nonstreaming
OUTPUT_FORMAT: {
"AUDIO_24KHZ_48KBITRATE_MONO_MP3": "audio-24khz-48kbitrate-mono-mp3",
"AUDIO_24KHZ_96KBITRATE_MONO_MP3": "audio-24khz-96kbitrate-mono-mp3",
"WEBM_24KHZ_16BIT_MONO_OPUS": "webm-24khz-16bit-mono-opus"
}
};
// src/browser/EdgeTTS.browser.ts
var EdgeTTS = class {
constructor() {
__publicField(this, "audio_stream", []);
__publicField(this, "ws");
}
async normalizeVoices(data) {
const out = [];
for (const v of data || []) {
const short = v?.ShortName || "";
const locale = v?.Locale || "";
let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
const mix = `${v?.Name || ""} ${short}`;
const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
const localeName = v?.LocaleName || (locale || null);
let display = v?.DisplayName || v?.FriendlyName || base || short;
display = display.replace(/^Microsoft\s+/i, "");
display = display.split(" - ")[0].trim();
display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
display = display.replace(/\s*Online\s*/i, " ");
display = display.replace(/\s+/g, " ").trim();
const tag = v?.VoiceTag && typeof v.VoiceTag === "object" ? v.VoiceTag : {};
const tailored = Array.isArray(tag.TailoredScenarios) ? tag.TailoredScenarios : Array.isArray(tag.ContentCategories) ? tag.ContentCategories : [];
const personalities = Array.isArray(tag.VoicePersonalities) ? tag.VoicePersonalities : [];
out.push({
Name: short || (v?.Name || ""),
DisplayName: display,
LocalName: display,
ShortName: short || (v?.Name || ""),
Gender: v?.Gender ?? null,
Locale: locale || null,
LocaleName: localeName,
SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
VoiceType: voiceType,
VoiceTag: {
TailoredScenarios: tailored,
VoicePersonalities: personalities
},
FriendlyName: `${display} (${voiceType}) - ${localeName}`
});
}
return out;
}
async getVoices() {
const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
const response = await fetch(
`${Constants.VOICES_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}`,
{ headers: Constants.getBaseHeaders() }
);
const data = await response.json();
return this.normalizeVoices(data.voices || []);
}
async getVoicesByLanguage(locale) {
const voices = await this.getVoices();
return voices.filter((voice) => voice.Locale.startsWith(locale));
}
async getVoicesByGender(gender) {
const voices = await this.getVoices();
return voices.filter((voice) => voice.Gender === gender);
}
generateUUID() {
return "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 3 | 8;
return v.toString(16);
});
}
validatePitch(pitch) {
if (typeof pitch === "number") {
return pitch >= 0 ? `+${pitch}Hz` : `${pitch}Hz`;
}
return pitch;
}
validateRate(rate) {
let rateValue;
if (typeof rate === "string") {
rateValue = parseFloat(rate.replace("%", ""));
} else {
rateValue = rate;
}
return rateValue >= 0 ? `+${rateValue}%` : `${rateValue}%`;
}
validateVolume(volume) {
let volumeValue;
if (typeof volume === "string") {
volumeValue = parseInt(volume.replace("%", ""), 10);
} else {
volumeValue = volume;
}
return `${volumeValue}%`;
}
escapeXML(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
getSSML(content, voice, options = {}) {
const pitch = this.validatePitch(options.pitch ?? 0);
const rate = this.validateRate(options.rate ?? 0);
const volume = this.validateVolume(options.volume ?? 0);
const escapedText = this.escapeXML(content);
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="en-US">
<voice name="${voice}">
<prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
${escapedText}
</prosody>
</voice>
</speak>`;
}
nowRFC1123() {
return (/* @__PURE__ */ new Date()).toUTCString();
}
parseRFC1123(rfcStr) {
return new Date(rfcStr);
}
buildTTSConfigMessage() {
const timestamp = this.nowRFC1123();
return `X-Timestamp:${timestamp}\r
Content-Type:application/json; charset=utf-8\r
Path:speech.config\r
\r
{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}`;
}
async synthesize(text, voice = "en-US-JennyNeural", options = {}) {
const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
return new Promise((resolve, reject) => {
this.audio_stream = [];
const reqId = this.generateUUID();
const url = `${Constants.WSS_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
this.ws = new WebSocket(url);
const SSML_text = this.getSSML(text, voice, options);
let timedOut = false;
let inactivityTimeout;
const resetInactivityTimeout = () => {
clearTimeout(inactivityTimeout);
inactivityTimeout = window.setTimeout(() => {
timedOut = true;
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
}
reject(new Error("WebSocket inactivity timeout - no response from server"));
}, 3e4);
};
this.ws.onopen = () => {
resetInactivityTimeout();
const message = this.buildTTSConfigMessage();
this.ws.send(message);
const timestamp = this.nowRFC1123();
const speechMessage = `X-RequestId:${reqId}\r
Content-Type:application/ssml+xml\r
X-Timestamp:${timestamp}\r
Path:ssml\r
\r
${SSML_text}`;
this.ws.send(speechMessage);
};
this.ws.onmessage = (event) => {
resetInactivityTimeout();
if (typeof event.data === "string") {
if (event.data.includes("Path:turn.end")) {
this.ws?.close();
}
} else {
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const uint8Array = new Uint8Array(arrayBuffer);
this.processAudioData(uint8Array);
};
reader.readAsArrayBuffer(event.data);
}
};
this.ws.onerror = (err) => {
clearTimeout(inactivityTimeout);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
}
reject(new Error("WebSocket error"));
};
this.ws.onclose = () => {
clearTimeout(inactivityTimeout);
if (!timedOut) {
resolve();
}
};
});
}
async *synthesizeStream(text, voice = "en-US-JennyNeural", options = {}) {
this.audio_stream = [];
const reqId = this.generateUUID();
const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
const url = `${Constants.WSS_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
this.ws = new WebSocket(url);
const SSML_text = this.getSSML(text, voice, options);
const queue = [];
let done = false;
let error = null;
let notify = null;
const push = (chunk) => {
queue.push(chunk);
if (notify) {
notify();
notify = null;
}
};
let timedOut = false;
let inactivityTimeout;
const resetInactivityTimeout = () => {
clearTimeout(inactivityTimeout);
inactivityTimeout = window.setTimeout(() => {
timedOut = true;
error = new Error("WebSocket inactivity timeout - no response from server");
done = true;
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
}
if (notify) {
notify();
notify = null;
}
}, 3e4);
};
this.ws.onopen = () => {
resetInactivityTimeout();
const message = this.buildTTSConfigMessage();
this.ws.send(message);
const timestamp = this.nowRFC1123();
const speechMessage = `X-RequestId:${reqId}\r
Content-Type:application/ssml+xml\r
X-Timestamp:${timestamp}\r
Path:ssml\r
\r
${SSML_text}`;
this.ws.send(speechMessage);
};
this.ws.onmessage = (event) => {
resetInactivityTimeout();
if (typeof event.data === "string") {
if (event.data.includes("Path:turn.end")) {
this.ws?.close();
}
} else {
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const uint8Array = new Uint8Array(arrayBuffer);
const needle = new TextEncoder().encode("Path:audio\r\n");
const audioStartIndex = this.indexOf(uint8Array, needle);
if (audioStartIndex !== -1) {
const audioChunk = uint8Array.slice(audioStartIndex + needle.length);
this.audio_stream.push(audioChunk);
push(audioChunk);
}
};
reader.readAsArrayBuffer(event.data);
}
};
this.ws.onerror = (err) => {
clearTimeout(inactivityTimeout);
error = new Error("WebSocket error");
done = true;
if (notify) {
notify();
notify = null;
}
};
this.ws.onclose = () => {
clearTimeout(inactivityTimeout);
done = true;
if (notify) {
notify();
notify = null;
}
};
while (!done || queue.length > 0) {
if (queue.length === 0) {
await new Promise((resolve) => notify = resolve);
continue;
}
const chunk = queue.shift();
if (chunk) {
yield chunk;
}
}
if (error) {
throw error;
}
}
processAudioData(buffer) {
const needle = new TextEncoder().encode("Path:audio\r\n");
const audioStartIndex = this.indexOf(buffer, needle);
if (audioStartIndex !== -1) {
const audioChunk = buffer.slice(audioStartIndex + needle.length);
this.audio_stream.push(audioChunk);
}
}
indexOf(arr, subarr) {
for (let i = 0; i <= arr.length - subarr.length; i++) {
let match = true;
for (let j = 0; j < subarr.length; j++) {
if (arr[i + j] !== subarr[j]) {
match = false;
break;
}
}
if (match) return i;
}
return -1;
}
async generateSecMsGec(trustedClientToken) {
const now = this.nowRFC1123();
const fixedDate = this.parseRFC1123(now);
const ticks = Math.floor(fixedDate.getTime() / 1e3) + 11644473600;
const rounded = ticks - ticks % 300;
const windowsTicks = rounded * 1e7;
const encoder = new TextEncoder();
const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
}
getAudioData() {
if (this.audio_stream.length === 0) {
throw new Error("No audio data available");
}
const totalLength = this.audio_stream.reduce((acc, chunk) => acc + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of this.audio_stream) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
};
export {
EdgeTTS
};
//# sourceMappingURL=edge-tts.esm.js.map