UNPKG

@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
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"); } 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