UNPKG

edge-tts-universal

Version:

Universal text-to-speech library using Microsoft Edge's online TTS service. Works in Node.js and browsers WITHOUT needing Microsoft Edge, Windows, or an API key

836 lines (824 loc) 28.8 kB
// src/isomorphic-utils.ts function connectId() { const array = new Uint8Array(16); globalThis.crypto.getRandomValues(array); array[6] = array[6] & 15 | 64; array[8] = array[8] & 63 | 128; const hex = Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(""); const uuid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; return uuid.replace(/-/g, ""); } function escape(text) { return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"); } function unescape(text) { return text.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&"); } function dateToString(date) { if (!date) { date = /* @__PURE__ */ new Date(); } return date.toISOString().replace(/[-:.]/g, "").slice(0, -1); } function removeIncompatibleCharacters(str) { const chars_to_remove = '*/()[]{}$%^@#+=|\\~`><"&'; let clean_str = str; for (const char of chars_to_remove) { clean_str = clean_str.replace(new RegExp("\\" + char, "g"), ""); } return clean_str; } function mkssml(tc, escapedText) { const text = escapedText instanceof Uint8Array ? new TextDecoder().decode(escapedText) : escapedText; return `<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'><voice name='${tc.voice}'><prosody pitch='${tc.pitch}' rate='${tc.rate}' volume='${tc.volume}'>${text}</prosody></voice></speak>`; } function splitTextByByteLength(text, byteLength) { const encoder = new TextEncoder(); const words = text.split(/(\s+)/); const chunks = []; let currentChunk = ""; for (const word of words) { const potentialChunk = currentChunk + word; if (encoder.encode(potentialChunk).length <= byteLength) { currentChunk = potentialChunk; } else { if (currentChunk) { chunks.push(currentChunk.trim()); currentChunk = word; } else { const wordBytes = encoder.encode(word); for (let i = 0; i < wordBytes.length; i += byteLength) { const slice = wordBytes.slice(i, i + byteLength); chunks.push(new TextDecoder().decode(slice)); } currentChunk = ""; } } } if (currentChunk.trim()) { chunks.push(currentChunk.trim()); } return chunks; } function ssmlHeadersPlusData(requestId, timestamp, ssml) { return `X-RequestId:${requestId}\r Content-Type:application/ssml+xml\r X-Timestamp:${timestamp}Z\r Path:ssml\r \r ${ssml}`; } // src/exceptions.ts var EdgeTTSException = class extends Error { constructor(message) { super(message); this.name = "EdgeTTSException"; } }; var SkewAdjustmentError = class extends EdgeTTSException { constructor(message) { super(message); this.name = "SkewAdjustmentError"; } }; var UnknownResponse = class extends EdgeTTSException { constructor(message) { super(message); this.name = "UnknownResponse"; } }; var UnexpectedResponse = class extends EdgeTTSException { constructor(message) { super(message); this.name = "UnexpectedResponse"; } }; var NoAudioReceived = class extends EdgeTTSException { constructor(message) { super(message); this.name = "NoAudioReceived"; } }; var WebSocketError = class extends EdgeTTSException { constructor(message) { super(message); this.name = "WebSocketError"; } }; var ValueError = class extends EdgeTTSException { constructor(message) { super(message); this.name = "ValueError"; } }; // src/tts_config.ts var TTSConfig = class _TTSConfig { /** * Creates a new TTSConfig instance with the specified parameters. * * @param options - Configuration options * @param options.voice - Voice name (supports both short and full formats) * @param options.rate - Speech rate adjustment (default: "+0%") * @param options.volume - Volume adjustment (default: "+0%") * @param options.pitch - Pitch adjustment (default: "+0Hz") * @throws {ValueError} If any parameter has an invalid format */ constructor({ voice, rate = "+0%", volume = "+0%", pitch = "+0Hz" }) { this.voice = voice; this.rate = rate; this.volume = volume; this.pitch = pitch; this.validate(); } validate() { const match = /^([a-z]{2,})-([A-Z]{2,})-(.+Neural)$/.exec(this.voice); if (match) { const [, lang] = match; let [, , region, name] = match; if (name.includes("-")) { const parts = name.split("-"); region += `-${parts[0]}`; name = parts[1]; } this.voice = `Microsoft Server Speech Text to Speech Voice (${lang}-${region}, ${name})`; } _TTSConfig.validateStringParam( "voice", this.voice, /^Microsoft Server Speech Text to Speech Voice \(.+,.+\)$/ ); _TTSConfig.validateStringParam("rate", this.rate, /^[+-]\d+%$/); _TTSConfig.validateStringParam("volume", this.volume, /^[+-]\d+%$/); _TTSConfig.validateStringParam("pitch", this.pitch, /^[+-]\d+Hz$/); } static validateStringParam(paramName, paramValue, pattern) { if (typeof paramValue !== "string") { throw new TypeError(`${paramName} must be a string`); } if (!pattern.test(paramValue)) { throw new ValueError(`Invalid ${paramName} '${paramValue}'.`); } } }; // src/constants.ts var BASE_URL = "speech.platform.bing.com/consumer/speech/synthesize/readaloud"; var TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4"; var WSS_URL = `wss://${BASE_URL}/edge/v1?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}`; var VOICE_LIST_URL = `https://${BASE_URL}/voices/list?trustedclienttoken=${TRUSTED_CLIENT_TOKEN}`; var DEFAULT_VOICE = "en-US-EmmaMultilingualNeural"; var CHROMIUM_FULL_VERSION = "130.0.2849.68"; var CHROMIUM_MAJOR_VERSION = CHROMIUM_FULL_VERSION.split(".")[0]; var SEC_MS_GEC_VERSION = `1-${CHROMIUM_FULL_VERSION}`; var BASE_HEADERS = { "User-Agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_MAJOR_VERSION}.0.0.0 Safari/537.36 Edg/${CHROMIUM_MAJOR_VERSION}.0.0.0`, "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9" }; var WSS_HEADERS = { ...BASE_HEADERS, "Pragma": "no-cache", "Cache-Control": "no-cache", "Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold" }; var VOICE_HEADERS = { ...BASE_HEADERS, "Authority": "speech.platform.bing.com", "Sec-CH-UA": `" Not;A Brand";v="99", "Microsoft Edge";v="${CHROMIUM_MAJOR_VERSION}", "Chromium";v="${CHROMIUM_MAJOR_VERSION}"`, "Sec-CH-UA-Mobile": "?0", "Accept": "*/*", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty" }; // src/isomorphic-drm.ts var WIN_EPOCH = 11644473600; var S_TO_NS = 1e9; var _IsomorphicDRM = class _IsomorphicDRM { static adjClockSkewSeconds(skewSeconds) { _IsomorphicDRM.clockSkewSeconds += skewSeconds; } static getUnixTimestamp() { return Date.now() / 1e3 + _IsomorphicDRM.clockSkewSeconds; } static parseRfc2616Date(date) { try { return new Date(date).getTime() / 1e3; } catch (e) { return null; } } static handleClientResponseError(response) { let serverDate = null; if ("headers" in response && typeof response.headers === "object") { if ("get" in response.headers && typeof response.headers.get === "function") { serverDate = response.headers.get("date"); } else { const headers = response.headers; serverDate = headers["date"] || headers["Date"]; } } if (!serverDate) { throw new SkewAdjustmentError("No server date in headers."); } const serverDateParsed = _IsomorphicDRM.parseRfc2616Date(serverDate); if (serverDateParsed === null) { throw new SkewAdjustmentError(`Failed to parse server date: ${serverDate}`); } const clientDate = _IsomorphicDRM.getUnixTimestamp(); _IsomorphicDRM.adjClockSkewSeconds(serverDateParsed - clientDate); } static async generateSecMsGec() { let ticks = _IsomorphicDRM.getUnixTimestamp(); ticks += WIN_EPOCH; ticks -= ticks % 300; ticks *= S_TO_NS / 100; const strToHash = `${ticks.toFixed(0)}${TRUSTED_CLIENT_TOKEN}`; if (!globalThis.crypto || !globalThis.crypto.subtle) { throw new Error("Web Crypto API not available"); } const encoder = new TextEncoder(); const data = encoder.encode(strToHash); const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase(); } }; _IsomorphicDRM.clockSkewSeconds = 0; var IsomorphicDRM = _IsomorphicDRM; // src/isomorphic-communicate.ts var IsomorphicBuffer = { from: (input, encoding) => { if (typeof input === "string") { return new TextEncoder().encode(input); } else if (input instanceof ArrayBuffer) { return new Uint8Array(input); } else if (input instanceof Uint8Array) { return input; } throw new Error("Unsupported input type for IsomorphicBuffer.from"); }, concat: (arrays) => { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; }, isBuffer: (obj) => { return obj instanceof Uint8Array; }, toString: (buffer, encoding) => { return new TextDecoder(encoding || "utf-8").decode(buffer); } }; function isomorphicGetHeadersAndDataFromText(message) { const messageString = IsomorphicBuffer.toString(message); const headerEndIndex = messageString.indexOf("\r\n\r\n"); const headers = {}; if (headerEndIndex !== -1) { const headerString = messageString.substring(0, headerEndIndex); const headerLines = headerString.split("\r\n"); for (const line of headerLines) { const [key, value] = line.split(":", 2); if (key && value) { headers[key] = value.trim(); } } } const headerByteLength = new TextEncoder().encode(messageString.substring(0, headerEndIndex + 4)).length; return [headers, message.slice(headerByteLength)]; } function isomorphicGetHeadersAndDataFromBinary(message) { if (message.length < 2) { throw new Error("Message too short to contain header length"); } const headerLength = message[0] << 8 | message[1]; const headers = {}; if (headerLength > 0 && headerLength + 2 <= message.length) { const headerBytes = message.slice(2, headerLength + 2); const headerString = IsomorphicBuffer.toString(headerBytes); const headerLines = headerString.split("\r\n"); for (const line of headerLines) { const [key, value] = line.split(":", 2); if (key && value) { headers[key] = value.trim(); } } } return [headers, message.slice(headerLength + 2)]; } var IsomorphicCommunicate = class { /** * Creates a new isomorphic Communicate instance for text-to-speech synthesis. * * @param text - The text to synthesize * @param options - Configuration options for synthesis */ constructor(text, options = {}) { // Universal build - proxy and environment detection removed for compatibility this.state = { partialText: IsomorphicBuffer.from(""), offsetCompensation: 0, lastDurationOffset: 0, streamWasCalled: false }; this.ttsConfig = new TTSConfig({ voice: options.voice || DEFAULT_VOICE, rate: options.rate, volume: options.volume, pitch: options.pitch }); if (typeof text !== "string") { throw new TypeError("text must be a string"); } const processedText = escape(removeIncompatibleCharacters(text)); const maxSize = 4096; this.texts = (function* () { for (const chunk of splitTextByByteLength(processedText, maxSize)) { yield new TextEncoder().encode(chunk); } })(); } parseMetadata(data) { const metadata = JSON.parse(IsomorphicBuffer.toString(data)); for (const metaObj of metadata["Metadata"]) { const metaType = metaObj["Type"]; if (metaType === "WordBoundary") { const currentOffset = metaObj["Data"]["Offset"] + this.state.offsetCompensation; const currentDuration = metaObj["Data"]["Duration"]; return { type: metaType, offset: currentOffset, duration: currentDuration, text: unescape(metaObj["Data"]["text"]["Text"]) }; } if (metaType === "SessionEnd") { continue; } throw new UnknownResponse(`Unknown metadata type: ${metaType}`); } throw new UnexpectedResponse("No WordBoundary metadata found"); } async createWebSocket(url) { const isNode = typeof globalThis !== "undefined" ? globalThis.process?.versions?.node !== void 0 : typeof process !== "undefined" && process.versions?.node !== void 0; if (isNode) { try { const { default: WS } = await import('ws'); return new WS(url, { headers: WSS_HEADERS }); } catch (error) { console.warn("ws library not available, using native WebSocket without headers"); return new WebSocket(url); } } else { return new WebSocket(url); } } async *_stream() { const url = `${WSS_URL}&Sec-MS-GEC=${await IsomorphicDRM.generateSecMsGec()}&Sec-MS-GEC-Version=${SEC_MS_GEC_VERSION}&ConnectionId=${connectId()}`; const websocket = await this.createWebSocket(url); const messageQueue = []; let resolveMessage = null; const handleMessage = (message, isBinary) => { const data = message.data || message; const binary = isBinary ?? (data instanceof ArrayBuffer || data instanceof Uint8Array); if (!binary && typeof data === "string") { const [headers, parsedData] = isomorphicGetHeadersAndDataFromText(IsomorphicBuffer.from(data)); const path = headers["Path"]; if (path === "audio.metadata") { try { const parsedMetadata = this.parseMetadata(parsedData); this.state.lastDurationOffset = parsedMetadata.offset + parsedMetadata.duration; messageQueue.push(parsedMetadata); } catch (e) { messageQueue.push(e); } } else if (path === "turn.end") { this.state.offsetCompensation = this.state.lastDurationOffset; websocket.close(); } else if (path !== "response" && path !== "turn.start") { messageQueue.push(new UnknownResponse(`Unknown path received: ${path}`)); } } else { let bufferData; if (data instanceof ArrayBuffer) { bufferData = IsomorphicBuffer.from(data); } else if (data instanceof Uint8Array) { bufferData = data; } else if (typeof Buffer !== "undefined" && data instanceof Buffer) { bufferData = new Uint8Array(data); } else if (typeof Blob !== "undefined" && data instanceof Blob) { data.arrayBuffer().then((arrayBuffer) => { const blobBufferData = new Uint8Array(arrayBuffer); processBinaryData(blobBufferData); }).catch((error) => { messageQueue.push(new UnexpectedResponse(`Failed to process Blob data: ${error.message}`)); if (resolveMessage) resolveMessage(); }); return; } else { messageQueue.push(new UnexpectedResponse(`Unknown binary data type: ${typeof data} ${data.constructor?.name}`)); return; } processBinaryData(bufferData); } if (resolveMessage) resolveMessage(); }; const processBinaryData = (bufferData) => { if (bufferData.length < 2) { messageQueue.push(new UnexpectedResponse("We received a binary message, but it is missing the header length.")); } else { const [headers, audioData] = isomorphicGetHeadersAndDataFromBinary(bufferData); if (headers["Path"] !== "audio") { messageQueue.push(new UnexpectedResponse("Received binary message, but the path is not audio.")); } else { const contentType = headers["Content-Type"]; if (contentType !== "audio/mpeg") { if (audioData.length > 0) { messageQueue.push(new UnexpectedResponse("Received binary message, but with an unexpected Content-Type.")); } } else if (audioData.length === 0) { messageQueue.push(new UnexpectedResponse("Received binary message, but it is missing the audio data.")); } else { messageQueue.push({ type: "audio", data: audioData }); } } } }; websocket.onmessage = handleMessage; websocket.onerror = (error) => { messageQueue.push(new WebSocketError(error.message || "WebSocket error")); if (resolveMessage) resolveMessage(); }; websocket.onclose = () => { messageQueue.push("close"); if (resolveMessage) resolveMessage(); }; await new Promise((resolve, reject) => { const onOpen = () => resolve(); const onError = (error) => reject(error); websocket.onopen = onOpen; websocket.onerror = onError; }); websocket.send( `X-Timestamp:${dateToString()}\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"}}}}\r ` ); websocket.send( ssmlHeadersPlusData( connectId(), dateToString(), mkssml(this.ttsConfig, IsomorphicBuffer.toString(this.state.partialText)) ) ); let audioWasReceived = false; while (true) { if (messageQueue.length > 0) { const message = messageQueue.shift(); if (message === "close") { if (!audioWasReceived) { throw new NoAudioReceived("No audio was received."); } break; } else if (message instanceof Error) { throw message; } else { if (message.type === "audio") audioWasReceived = true; yield message; } } else { await new Promise((resolve) => { resolveMessage = resolve; setTimeout(resolve, 50); }); } } } /** * Streams text-to-speech synthesis results using isomorphic WebSocket. * Works in both Node.js and browsers (subject to CORS policy). * * @yields TTSChunk - Audio data or word boundary information * @throws {Error} If called more than once * @throws {NoAudioReceived} If no audio data is received * @throws {WebSocketError} If WebSocket connection fails */ async *stream() { if (this.state.streamWasCalled) { throw new Error("stream can only be called once."); } this.state.streamWasCalled = true; for (const partialText of this.texts) { this.state.partialText = partialText; for await (const message of this._stream()) { yield message; } } } }; // src/isomorphic-voices.ts var FetchError = class extends Error { constructor(message, response) { super(message); this.name = "FetchError"; this.response = response; } }; async function _listVoices(proxy) { const url = `${VOICE_LIST_URL}&Sec-MS-GEC=${await IsomorphicDRM.generateSecMsGec()}&Sec-MS-GEC-Version=${SEC_MS_GEC_VERSION}`; const fetchOptions = { headers: VOICE_HEADERS }; if (proxy) { console.warn("Proxy support in isomorphic environment is limited. Consider using a backend proxy."); } try { const response = await fetch(url, fetchOptions); if (!response.ok) { const headers = {}; response.headers.forEach((value, key) => { headers[key] = value; }); throw new FetchError(`HTTP ${response.status}`, { status: response.status, headers }); } const data = await response.json(); for (const voice of data) { voice.VoiceTag.ContentCategories = voice.VoiceTag.ContentCategories.map((c) => c.trim()); voice.VoiceTag.VoicePersonalities = voice.VoiceTag.VoicePersonalities.map((p) => p.trim()); } return data; } catch (error) { if (error instanceof FetchError) { throw error; } throw new FetchError(error instanceof Error ? error.message : "Unknown fetch error"); } } async function listVoices(proxy) { try { return await _listVoices(proxy); } catch (e) { if (e instanceof FetchError && e.response?.status === 403) { IsomorphicDRM.handleClientResponseError(e.response); return await _listVoices(proxy); } throw e; } } var IsomorphicVoicesManager = class _IsomorphicVoicesManager { constructor() { this.voices = []; this.calledCreate = false; } /** * Creates a new IsomorphicVoicesManager instance. * * @param customVoices - Optional custom voice list instead of fetching from API * @param proxy - Optional proxy URL for API requests (limited browser support) * @returns Promise resolving to IsomorphicVoicesManager instance */ static async create(customVoices, proxy) { const manager = new _IsomorphicVoicesManager(); const voices = customVoices ?? await listVoices(proxy); manager.voices = voices.map((voice) => ({ ...voice, Language: voice.Locale.split("-")[0] })); manager.calledCreate = true; return manager; } /** * Finds voices matching the specified criteria. * * @param filter - Filter criteria for voice selection * @returns Array of voices matching the filter * @throws {Error} If called before create() */ find(filter) { if (!this.calledCreate) { throw new Error("IsomorphicVoicesManager.find() called before IsomorphicVoicesManager.create()"); } return this.voices.filter((voice) => { return Object.entries(filter).every(([key, value]) => { return voice[key] === value; }); }); } }; // src/isomorphic-simple.ts function concatUint8Arrays(arrays) { if (arrays.length === 0) return new Uint8Array(0); if (arrays.length === 1) return arrays[0]; const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { if (arr.length > 0) { result.set(arr, offset); offset += arr.length; } } return result; } var IsomorphicEdgeTTS = class { /** * @param text The text to be synthesized. * @param voice The voice to use for synthesis. * @param options Prosody options (rate, volume, pitch). */ constructor(text, voice = "Microsoft Server Speech Text to Speech Voice (en-US, EmmaMultilingualNeural)", options = {}) { this.text = text; this.voice = voice; this.rate = options.rate || "+0%"; this.volume = options.volume || "+0%"; this.pitch = options.pitch || "+0Hz"; } /** * Initiates the synthesis process using isomorphic implementations. * @returns A promise that resolves with the synthesized audio and subtitle data. */ async synthesize() { const communicate = new IsomorphicCommunicate(this.text, { voice: this.voice, rate: this.rate, volume: this.volume, pitch: this.pitch }); const audioChunks = []; const wordBoundaries = []; for await (const chunk of communicate.stream()) { if (chunk.type === "audio" && chunk.data) { audioChunks.push(chunk.data); } else if (chunk.type === "WordBoundary" && chunk.offset !== void 0 && chunk.duration !== void 0 && chunk.text !== void 0) { wordBoundaries.push({ offset: chunk.offset, duration: chunk.duration, text: chunk.text }); } } const audioBuffer = concatUint8Arrays(audioChunks); const audioBlob = new Blob([ audioBuffer ], { type: "audio/mpeg" }); return { audio: audioBlob, subtitle: wordBoundaries }; } }; function formatTimestamp(timeIn100ns, format) { const totalSeconds = Math.floor(timeIn100ns / 1e7); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor(totalSeconds % 3600 / 60); const seconds = totalSeconds % 60; const milliseconds = Math.floor(timeIn100ns % 1e7 / 1e4); const separator = format === "vtt" ? "." : ","; return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(seconds)}${separator}${padNumber(milliseconds, 3)}`; } function padNumber(num, length = 2) { return num.toString().padStart(length, "0"); } function createVTT(wordBoundaries) { let vttContent = "WEBVTT\n\n"; wordBoundaries.forEach((word, index) => { const startTime = formatTimestamp(word.offset, "vtt"); const endTime = formatTimestamp(word.offset + word.duration, "vtt"); vttContent += `${index + 1} `; vttContent += `${startTime} --> ${endTime} `; vttContent += `${word.text} `; }); return vttContent; } function createSRT(wordBoundaries) { let srtContent = ""; wordBoundaries.forEach((word, index) => { const startTime = formatTimestamp(word.offset, "srt"); const endTime = formatTimestamp(word.offset + word.duration, "srt"); srtContent += `${index + 1} `; srtContent += `${startTime} --> ${endTime} `; srtContent += `${word.text} `; }); return srtContent; } // src/submaker.ts function formatTime(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor(seconds % 3600 / 60); const s = Math.floor(seconds % 60); const ms = Math.round((seconds - Math.floor(seconds)) * 1e3); const pad = (num, size = 2) => num.toString().padStart(size, "0"); return `${pad(h)}:${pad(m)}:${pad(s)},${pad(ms, 3)}`; } var SubMaker = class { constructor() { this.cues = []; } /** * Adds a WordBoundary chunk to the subtitle maker. * * @param msg - Must be a WordBoundary type chunk with offset, duration, and text * @throws {ValueError} If chunk is not a WordBoundary with required fields */ feed(msg) { if (msg.type !== "WordBoundary" || msg.offset === void 0 || msg.duration === void 0 || msg.text === void 0) { throw new ValueError("Invalid message type, expected 'WordBoundary' with offset, duration and text"); } const start = msg.offset / 1e7; const end = (msg.offset + msg.duration) / 1e7; this.cues.push({ index: this.cues.length + 1, start, end, content: msg.text }); } /** * Merges consecutive cues to create subtitle entries with multiple words. * This is useful for creating more readable subtitles instead of word-by-word display. * * @param words - Maximum number of words per merged cue * @throws {ValueError} If words parameter is invalid */ mergeCues(words) { if (words <= 0) { throw new ValueError("Invalid number of words to merge, expected > 0"); } if (this.cues.length === 0) { return; } const newCues = []; let currentCue = this.cues[0]; for (const cue of this.cues.slice(1)) { if (currentCue.content.split(" ").length < words) { currentCue = { ...currentCue, end: cue.end, content: `${currentCue.content} ${cue.content}` }; } else { newCues.push(currentCue); currentCue = cue; } } newCues.push(currentCue); this.cues = newCues.map((cue, i) => ({ ...cue, index: i + 1 })); } /** * Returns the subtitles in SRT format. * * @returns SRT formatted subtitles */ getSrt() { return this.cues.map((cue) => { return `${cue.index}\r ${formatTime(cue.start)} --> ${formatTime(cue.end)}\r ${cue.content}\r `; }).join("\r\n"); } toString() { return this.getSrt(); } }; // src/webworker-entry.ts function isWebWorker() { return typeof globalThis.importScripts === "function" && typeof globalThis.WorkerGlobalScope !== "undefined"; } function postAudioMessage(audio, subtitle) { if (isWebWorker()) { globalThis.postMessage({ type: "tts-result", audio, subtitle }); } else { console.warn("postAudioMessage should only be called in Web Worker context"); } } export { IsomorphicCommunicate as Communicate, IsomorphicDRM as DRM, IsomorphicEdgeTTS as EdgeTTS, EdgeTTSException, FetchError, NoAudioReceived, SkewAdjustmentError, SubMaker, UnexpectedResponse, UnknownResponse, ValueError, IsomorphicVoicesManager as VoicesManager, WebSocketError, createSRT, createVTT, isWebWorker, listVoices, postAudioMessage }; //# sourceMappingURL=webworker.js.map //# sourceMappingURL=webworker.js.map