@ssml-builder-js/azure-tts-client 2.18.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/dist/index.js +453 -63
- package/package.json +8 -8
- package/src/client.ts +43 -3
- package/src/deadline.ts +52 -0
- package/src/errors.ts +86 -4
- package/src/index.ts +11 -1
- package/src/safe.ts +52 -31
- package/src/synthesis.ts +307 -22
- package/src/types.ts +8 -3
- package/test/synthesis.test.ts +20 -0
- package/test/v219-pipeline.test.ts +265 -0
- package/dist/index.d.mts +0 -487
- package/dist/index.d.ts +0 -487
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1813
- package/dist/index.mjs.map +0 -1
package/dist/index.mjs
DELETED
|
@@ -1,1813 +0,0 @@
|
|
|
1
|
-
var __typeError = (msg) => {
|
|
2
|
-
throw TypeError(msg);
|
|
3
|
-
};
|
|
4
|
-
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
-
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
-
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
|
|
9
|
-
// src/errors.ts
|
|
10
|
-
var AzureTtsError = class extends Error {
|
|
11
|
-
constructor(status, statusText, responseBody, requestId, responseHeaders) {
|
|
12
|
-
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
13
|
-
this.kind = "azure-api-error";
|
|
14
|
-
this.name = "AzureTtsError";
|
|
15
|
-
this.status = status;
|
|
16
|
-
this.statusText = statusText;
|
|
17
|
-
this.responseBody = responseBody;
|
|
18
|
-
this.requestId = requestId;
|
|
19
|
-
const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
|
|
20
|
-
const seconds = value ? Number(value.trim()) : NaN;
|
|
21
|
-
const date = value ? Date.parse(value) : NaN;
|
|
22
|
-
if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
|
|
23
|
-
else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
function getRetryAfterDelayMs(error) {
|
|
27
|
-
if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
|
|
28
|
-
if (!error || typeof error !== "object") return void 0;
|
|
29
|
-
const candidate = error;
|
|
30
|
-
if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
|
|
31
|
-
const headers = candidate.headers ?? candidate.response?.headers;
|
|
32
|
-
if (headers instanceof Headers) {
|
|
33
|
-
const value = headers.get("retry-after");
|
|
34
|
-
if (!value) return void 0;
|
|
35
|
-
const seconds = Number(value.trim());
|
|
36
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
37
|
-
const date = Date.parse(value);
|
|
38
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
39
|
-
}
|
|
40
|
-
if (headers && typeof headers === "object") {
|
|
41
|
-
const value = headers["retry-after"] ?? headers["Retry-After"];
|
|
42
|
-
if (typeof value !== "string") return void 0;
|
|
43
|
-
const seconds = Number(value.trim());
|
|
44
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
45
|
-
const date = Date.parse(value);
|
|
46
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
47
|
-
}
|
|
48
|
-
return void 0;
|
|
49
|
-
}
|
|
50
|
-
var AzureTtsSdkError = class extends AzureTtsError {
|
|
51
|
-
constructor(errorDetails) {
|
|
52
|
-
super(0, "Speech SDK", errorDetails, null);
|
|
53
|
-
this.name = "AzureTtsSdkError";
|
|
54
|
-
this.message = `Azure TTS synthesis failed: ${errorDetails}`;
|
|
55
|
-
this.errorDetails = errorDetails;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var SynthesisCancelledError = class extends Error {
|
|
59
|
-
constructor(message = "Speech synthesis was cancelled.") {
|
|
60
|
-
super(message);
|
|
61
|
-
this.kind = "cancelled";
|
|
62
|
-
this.name = "SynthesisCancelledError";
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
var SynthesisTimeoutError = class extends Error {
|
|
66
|
-
constructor(message) {
|
|
67
|
-
super(message);
|
|
68
|
-
this.kind = "timeout";
|
|
69
|
-
this.name = "SynthesisTimeoutError";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
var MergeError = class extends Error {
|
|
73
|
-
constructor(message, cause) {
|
|
74
|
-
super(message);
|
|
75
|
-
this.kind = "merge-error";
|
|
76
|
-
this.name = "MergeError";
|
|
77
|
-
this.cause = cause;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
var AudioFormatMismatchError = class extends Error {
|
|
81
|
-
constructor(message, inputSpecs = []) {
|
|
82
|
-
super(message);
|
|
83
|
-
this.kind = "audio-format-mismatch";
|
|
84
|
-
this.name = "AudioFormatMismatchError";
|
|
85
|
-
this.inputSpecs = inputSpecs;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
var UnsupportedMergeFormatError = class extends Error {
|
|
89
|
-
constructor(format) {
|
|
90
|
-
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
91
|
-
this.kind = "unsupported-format-error";
|
|
92
|
-
this.name = "UnsupportedMergeFormatError";
|
|
93
|
-
this.format = format;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
function toSynthesisError(error) {
|
|
97
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
98
|
-
return error;
|
|
99
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
101
|
-
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
102
|
-
return createSpeechSdkError(error);
|
|
103
|
-
}
|
|
104
|
-
function createSpeechSdkError(error) {
|
|
105
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
-
return new AzureTtsSdkError(message);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// src/synthesis.ts
|
|
110
|
-
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
111
|
-
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
112
|
-
|
|
113
|
-
// src/outputFormats.ts
|
|
114
|
-
import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
|
|
115
|
-
var DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
|
|
116
|
-
var OUTPUT_FORMATS = {
|
|
117
|
-
"raw-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw,
|
|
118
|
-
"riff-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren,
|
|
119
|
-
"audio-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren,
|
|
120
|
-
"audio-16khz-32kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3,
|
|
121
|
-
"audio-16khz-128kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3,
|
|
122
|
-
"audio-16khz-64kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz64KBitRateMonoMp3,
|
|
123
|
-
"audio-24khz-48kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3,
|
|
124
|
-
"audio-24khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3,
|
|
125
|
-
"audio-24khz-160kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3,
|
|
126
|
-
"raw-16khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoTrueSilk,
|
|
127
|
-
"riff-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm,
|
|
128
|
-
"riff-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz16BitMonoPcm,
|
|
129
|
-
"riff-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm,
|
|
130
|
-
"riff-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw,
|
|
131
|
-
"raw-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm,
|
|
132
|
-
"raw-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm,
|
|
133
|
-
"raw-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm,
|
|
134
|
-
"ogg-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg16Khz16BitMonoOpus,
|
|
135
|
-
"ogg-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg24Khz16BitMonoOpus,
|
|
136
|
-
"raw-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw48Khz16BitMonoPcm,
|
|
137
|
-
"riff-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm,
|
|
138
|
-
"audio-48khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz96KBitRateMonoMp3,
|
|
139
|
-
"audio-48khz-192kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3,
|
|
140
|
-
"ogg-48khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg48Khz16BitMonoOpus,
|
|
141
|
-
"webm-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm16Khz16BitMonoOpus,
|
|
142
|
-
"webm-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16BitMonoOpus,
|
|
143
|
-
"webm-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16Bit24KbpsMonoOpus,
|
|
144
|
-
"raw-24khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoTrueSilk,
|
|
145
|
-
"raw-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoALaw,
|
|
146
|
-
"riff-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoALaw,
|
|
147
|
-
"audio-16khz-16bit-32kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16Bit32KbpsMonoOpus,
|
|
148
|
-
"audio-24khz-16bit-48kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit48KbpsMonoOpus,
|
|
149
|
-
"audio-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit24KbpsMonoOpus,
|
|
150
|
-
"raw-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm,
|
|
151
|
-
"riff-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff22050Hz16BitMonoPcm,
|
|
152
|
-
"raw-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw44100Hz16BitMonoPcm,
|
|
153
|
-
"riff-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm,
|
|
154
|
-
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
155
|
-
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
156
|
-
};
|
|
157
|
-
function resolveMimeType(outputFormat) {
|
|
158
|
-
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
159
|
-
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
160
|
-
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
161
|
-
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
162
|
-
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
163
|
-
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
164
|
-
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
165
|
-
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
166
|
-
return "application/octet-stream";
|
|
167
|
-
}
|
|
168
|
-
function resolveOutputFormat(outputFormat) {
|
|
169
|
-
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
170
|
-
if (resolvedFormat === void 0) {
|
|
171
|
-
throw new Error(`Unsupported Azure Speech output format: ${outputFormat}`);
|
|
172
|
-
}
|
|
173
|
-
return resolvedFormat;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/speechConfig.ts
|
|
177
|
-
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
178
|
-
function resolveEndpoint(config) {
|
|
179
|
-
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
180
|
-
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
181
|
-
}
|
|
182
|
-
function createSpeechConfig(config) {
|
|
183
|
-
const { outputFormat = DEFAULT_OUTPUT_FORMAT, subscriptionKey } = config;
|
|
184
|
-
const endpoint = new URL(resolveEndpoint(config));
|
|
185
|
-
const speechConfig = SpeechConfig.fromEndpoint(endpoint, subscriptionKey);
|
|
186
|
-
speechConfig.speechSynthesisOutputFormat = resolveOutputFormat(outputFormat);
|
|
187
|
-
return speechConfig;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/synthesis.ts
|
|
191
|
-
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT) {
|
|
192
|
-
const readAttribute = (name) => {
|
|
193
|
-
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
194
|
-
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
195
|
-
};
|
|
196
|
-
const payload = JSON.stringify({
|
|
197
|
-
ssml,
|
|
198
|
-
outputFormat,
|
|
199
|
-
voice: readAttribute("(?:name|voice)"),
|
|
200
|
-
language: readAttribute("(?:xml:lang|lang)"),
|
|
201
|
-
rate: readAttribute("rate"),
|
|
202
|
-
pitch: readAttribute("pitch")
|
|
203
|
-
});
|
|
204
|
-
let hash = 0xcbf29ce484222325n;
|
|
205
|
-
const mask = 0xffffffffffffffffn;
|
|
206
|
-
for (let index = 0; index < payload.length; index += 1) {
|
|
207
|
-
hash ^= BigInt(payload.charCodeAt(index));
|
|
208
|
-
hash = hash * 0x100000001b3n & mask;
|
|
209
|
-
}
|
|
210
|
-
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
211
|
-
}
|
|
212
|
-
function ascii(bytes, offset, value) {
|
|
213
|
-
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
214
|
-
}
|
|
215
|
-
function readUint32(bytes, offset) {
|
|
216
|
-
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
217
|
-
}
|
|
218
|
-
function parseWav(buffer) {
|
|
219
|
-
const bytes = new Uint8Array(buffer);
|
|
220
|
-
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
221
|
-
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
222
|
-
}
|
|
223
|
-
const chunks = [];
|
|
224
|
-
const dataParts = [];
|
|
225
|
-
let format;
|
|
226
|
-
let offset = 12;
|
|
227
|
-
while (offset < bytes.byteLength) {
|
|
228
|
-
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
229
|
-
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
230
|
-
const size = readUint32(bytes, offset + 4);
|
|
231
|
-
const dataStart = offset + 8;
|
|
232
|
-
const dataEnd = dataStart + size;
|
|
233
|
-
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
234
|
-
const data2 = bytes.slice(dataStart, dataEnd);
|
|
235
|
-
chunks.push({ id, data: data2 });
|
|
236
|
-
if (id === "fmt ") format ?? (format = data2);
|
|
237
|
-
if (id === "data") dataParts.push(data2);
|
|
238
|
-
offset = dataEnd + (size & 1);
|
|
239
|
-
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
240
|
-
}
|
|
241
|
-
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
242
|
-
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
243
|
-
const data = new Uint8Array(dataLength);
|
|
244
|
-
let dataOffset = 0;
|
|
245
|
-
for (const part of dataParts) {
|
|
246
|
-
data.set(part, dataOffset);
|
|
247
|
-
dataOffset += part.byteLength;
|
|
248
|
-
}
|
|
249
|
-
return { chunks, data, format };
|
|
250
|
-
}
|
|
251
|
-
function formatSampleRate(format) {
|
|
252
|
-
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
253
|
-
if (!match?.[1] || !match[2]) return 0;
|
|
254
|
-
const value = Number(match[1]);
|
|
255
|
-
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
256
|
-
}
|
|
257
|
-
function formatChannels(format, fallback) {
|
|
258
|
-
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
259
|
-
if (/mono|1ch/i.test(format)) return 1;
|
|
260
|
-
return fallback;
|
|
261
|
-
}
|
|
262
|
-
function formatAudioSpecification(format) {
|
|
263
|
-
const sampleRate = formatSampleRate(format);
|
|
264
|
-
const channels = formatChannels(format, 0);
|
|
265
|
-
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
266
|
-
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
267
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /mulaw|mu-law/i.test(format) ? "mulaw" : /alaw|a-law/i.test(format) ? "alaw" : /siren/i.test(format) ? "siren" : /pcm/i.test(format) ? "pcm" : "unknown";
|
|
268
|
-
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
269
|
-
const container = /(?:wav|wave|riff)/i.test(format) ? "riff-wave" : /mp3|mpeg/i.test(format) ? "mp3-raw" : /ogg/i.test(format) ? "ogg" : /webm/i.test(format) ? "webm" : /raw/i.test(format) ? "raw" : void 0;
|
|
270
|
-
return {
|
|
271
|
-
format,
|
|
272
|
-
mimeType: resolveMimeType(format),
|
|
273
|
-
codec,
|
|
274
|
-
sampleRate,
|
|
275
|
-
channels,
|
|
276
|
-
...bitrate ? { bitrate } : {},
|
|
277
|
-
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
278
|
-
...container ? { container } : {},
|
|
279
|
-
isVbr: /vbr/i.test(format),
|
|
280
|
-
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function parseMp3Specification(buffer, format) {
|
|
284
|
-
const bytes = stripMp3Tags(buffer);
|
|
285
|
-
const bitrates = [
|
|
286
|
-
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
287
|
-
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
288
|
-
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
289
|
-
];
|
|
290
|
-
const sampleRates = [
|
|
291
|
-
[44100, 48e3, 32e3],
|
|
292
|
-
[22050, 24e3, 16e3],
|
|
293
|
-
[11025, 12e3, 8e3]
|
|
294
|
-
];
|
|
295
|
-
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
296
|
-
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
297
|
-
const header = bytes[index + 1] ?? 0;
|
|
298
|
-
const versionBits = header >> 3 & 3;
|
|
299
|
-
const layer = header >> 1 & 3;
|
|
300
|
-
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
301
|
-
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
302
|
-
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
303
|
-
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
304
|
-
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
305
|
-
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
306
|
-
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
307
|
-
if (!sampleRate || !bitrateKbps) continue;
|
|
308
|
-
return {
|
|
309
|
-
format,
|
|
310
|
-
mimeType: "audio/mpeg",
|
|
311
|
-
codec: "mp3",
|
|
312
|
-
sampleRate,
|
|
313
|
-
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
314
|
-
bitrate: bitrateKbps * 1e3,
|
|
315
|
-
container: "mp3-raw",
|
|
316
|
-
isVbr: false,
|
|
317
|
-
isCompressed: true
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
return void 0;
|
|
321
|
-
}
|
|
322
|
-
function inspectAudioSpecification(buffer, format) {
|
|
323
|
-
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
324
|
-
const parsed = parseWav(buffer);
|
|
325
|
-
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
326
|
-
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
327
|
-
const sampleRate = view.getUint32(4, true);
|
|
328
|
-
const channels = view.getUint16(2, true);
|
|
329
|
-
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
330
|
-
const formatCode = view.getUint16(0, true);
|
|
331
|
-
const namedCodec = formatAudioSpecification(format).codec;
|
|
332
|
-
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
333
|
-
return {
|
|
334
|
-
format,
|
|
335
|
-
mimeType: "audio/wav",
|
|
336
|
-
codec,
|
|
337
|
-
sampleRate,
|
|
338
|
-
channels,
|
|
339
|
-
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
340
|
-
bitDepth: bitsPerSample,
|
|
341
|
-
container: "riff-wave",
|
|
342
|
-
isVbr: false,
|
|
343
|
-
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
347
|
-
const specification = formatAudioSpecification(format);
|
|
348
|
-
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
349
|
-
return specification;
|
|
350
|
-
}
|
|
351
|
-
function validateRawAudioBuffer(buffer, specification) {
|
|
352
|
-
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
353
|
-
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
354
|
-
}
|
|
355
|
-
if (specification.codec === "siren" || specification.codec === "silk") return;
|
|
356
|
-
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
357
|
-
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
function validateAudioSpecifications(specs) {
|
|
364
|
-
const first = specs[0];
|
|
365
|
-
if (!first) return;
|
|
366
|
-
const mismatch = specs.find(
|
|
367
|
-
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || spec.codec !== first.codec || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate || first.bitDepth !== void 0 && spec.bitDepth !== void 0 && spec.bitDepth !== first.bitDepth || first.container !== void 0 && spec.container !== void 0 && spec.container !== first.container || first.isVbr !== void 0 && spec.isVbr !== void 0 && spec.isVbr !== first.isVbr
|
|
368
|
-
);
|
|
369
|
-
if (mismatch)
|
|
370
|
-
throw new AudioFormatMismatchError(
|
|
371
|
-
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
372
|
-
specs
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
function isAudioFormatMismatch(error) {
|
|
376
|
-
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
377
|
-
}
|
|
378
|
-
function writeUint32(target, offset, value) {
|
|
379
|
-
new DataView(target.buffer).setUint32(offset, value, true);
|
|
380
|
-
}
|
|
381
|
-
function writeChunk(target, offset, id, data) {
|
|
382
|
-
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
383
|
-
writeUint32(target, offset + 4, data.byteLength);
|
|
384
|
-
target.set(data, offset + 8);
|
|
385
|
-
const end = offset + 8 + data.byteLength;
|
|
386
|
-
if (data.byteLength & 1) target[end] = 0;
|
|
387
|
-
return end + (data.byteLength & 1);
|
|
388
|
-
}
|
|
389
|
-
function mergeWavBuffers(buffers) {
|
|
390
|
-
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
391
|
-
const parsed = buffers.map(parseWav);
|
|
392
|
-
const first = parsed[0];
|
|
393
|
-
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
394
|
-
if (parsed.some(
|
|
395
|
-
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
396
|
-
))
|
|
397
|
-
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
398
|
-
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
399
|
-
const nonDataLength = first.chunks.reduce(
|
|
400
|
-
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
401
|
-
0
|
|
402
|
-
);
|
|
403
|
-
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
404
|
-
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
405
|
-
const output = new Uint8Array(outputLength);
|
|
406
|
-
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
407
|
-
writeUint32(output, 4, outputLength - 8);
|
|
408
|
-
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
409
|
-
let outputOffset = 12;
|
|
410
|
-
let dataWritten = false;
|
|
411
|
-
for (const chunk of first.chunks) {
|
|
412
|
-
if (chunk.id === "data") {
|
|
413
|
-
if (dataWritten) continue;
|
|
414
|
-
const data = new Uint8Array(dataLength);
|
|
415
|
-
let dataOffset = 0;
|
|
416
|
-
for (const item of parsed) {
|
|
417
|
-
data.set(item.data, dataOffset);
|
|
418
|
-
dataOffset += item.data.byteLength;
|
|
419
|
-
}
|
|
420
|
-
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
421
|
-
dataWritten = true;
|
|
422
|
-
} else {
|
|
423
|
-
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
427
|
-
return output.buffer;
|
|
428
|
-
}
|
|
429
|
-
function skipId3v2(bytes) {
|
|
430
|
-
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
431
|
-
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
432
|
-
const hasFooter = (bytes[5] & 16) !== 0;
|
|
433
|
-
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
434
|
-
}
|
|
435
|
-
function stripMp3Tags(buffer) {
|
|
436
|
-
const bytes = new Uint8Array(buffer);
|
|
437
|
-
const start = skipId3v2(bytes);
|
|
438
|
-
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
439
|
-
return bytes.slice(Math.min(start, end), end);
|
|
440
|
-
}
|
|
441
|
-
function isMp3Format(format) {
|
|
442
|
-
return /(?:mp3|mpeg)/i.test(format);
|
|
443
|
-
}
|
|
444
|
-
function isWavFormat(format) {
|
|
445
|
-
return /(?:wav|wave|riff)/i.test(format);
|
|
446
|
-
}
|
|
447
|
-
function isRawFormat(format) {
|
|
448
|
-
return /^raw(?:-|$)/i.test(format);
|
|
449
|
-
}
|
|
450
|
-
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType) {
|
|
451
|
-
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
452
|
-
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
453
|
-
}
|
|
454
|
-
const specification = inspectAudioSpecification(merged, format);
|
|
455
|
-
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
456
|
-
const firstInput = inputSpecs[0];
|
|
457
|
-
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
458
|
-
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
459
|
-
...inputSpecs,
|
|
460
|
-
specification
|
|
461
|
-
]);
|
|
462
|
-
}
|
|
463
|
-
if (isRawFormat(format)) {
|
|
464
|
-
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
465
|
-
if (merged.byteLength !== expectedSize) {
|
|
466
|
-
throw new MergeError(
|
|
467
|
-
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
validateRawAudioBuffer(merged, specification);
|
|
471
|
-
}
|
|
472
|
-
return specification;
|
|
473
|
-
}
|
|
474
|
-
function resolveMergeAudioFormat(format) {
|
|
475
|
-
if (isWavFormat(format)) return "wav";
|
|
476
|
-
if (isMp3Format(format)) return "mp3";
|
|
477
|
-
if (isRawFormat(format)) return "raw";
|
|
478
|
-
return void 0;
|
|
479
|
-
}
|
|
480
|
-
function canMergeAudioFormat(format) {
|
|
481
|
-
return resolveMergeAudioFormat(format) !== void 0;
|
|
482
|
-
}
|
|
483
|
-
function mergeAudioBuffers(buffers, options) {
|
|
484
|
-
const format = typeof options === "string" ? options : options?.format;
|
|
485
|
-
if (!format) throw new UnsupportedMergeFormatError("");
|
|
486
|
-
try {
|
|
487
|
-
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
488
|
-
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
489
|
-
if (isMp3Format(format)) {
|
|
490
|
-
const parts = buffers.map(stripMp3Tags);
|
|
491
|
-
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
492
|
-
let offset = 0;
|
|
493
|
-
for (const part of parts) {
|
|
494
|
-
output.set(part, offset);
|
|
495
|
-
offset += part.byteLength;
|
|
496
|
-
}
|
|
497
|
-
return output.buffer;
|
|
498
|
-
}
|
|
499
|
-
if (isRawFormat(format)) {
|
|
500
|
-
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
501
|
-
let offset = 0;
|
|
502
|
-
for (const buffer of buffers) {
|
|
503
|
-
output.set(new Uint8Array(buffer), offset);
|
|
504
|
-
offset += buffer.byteLength;
|
|
505
|
-
}
|
|
506
|
-
return output.buffer;
|
|
507
|
-
}
|
|
508
|
-
throw new UnsupportedMergeFormatError(format);
|
|
509
|
-
} catch (error) {
|
|
510
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
511
|
-
throw error;
|
|
512
|
-
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
function closeSpeechResources(speechConfig, synthesizer) {
|
|
516
|
-
try {
|
|
517
|
-
synthesizer.close();
|
|
518
|
-
} catch {
|
|
519
|
-
}
|
|
520
|
-
try {
|
|
521
|
-
speechConfig.close();
|
|
522
|
-
} catch {
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
526
|
-
async function synthesizeSsmlOnce(ssml, config) {
|
|
527
|
-
if (config.signal?.aborted) {
|
|
528
|
-
throw new SynthesisCancelledError();
|
|
529
|
-
}
|
|
530
|
-
const speechConfig = createSpeechConfig(config);
|
|
531
|
-
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
532
|
-
return await new Promise((resolve, reject) => {
|
|
533
|
-
let resourcesClosed = false;
|
|
534
|
-
let settled = false;
|
|
535
|
-
let timeout;
|
|
536
|
-
let abortHandler;
|
|
537
|
-
const cleanup = () => {
|
|
538
|
-
if (timeout) clearTimeout(timeout);
|
|
539
|
-
if (abortHandler) config.signal?.removeEventListener("abort", abortHandler);
|
|
540
|
-
};
|
|
541
|
-
const closeResources = () => {
|
|
542
|
-
if (resourcesClosed) return;
|
|
543
|
-
resourcesClosed = true;
|
|
544
|
-
closeSpeechResources(speechConfig, synthesizer);
|
|
545
|
-
};
|
|
546
|
-
const rejectWithError = (error) => {
|
|
547
|
-
if (settled) return;
|
|
548
|
-
settled = true;
|
|
549
|
-
cleanup();
|
|
550
|
-
closeResources();
|
|
551
|
-
reject(toSynthesisError(error));
|
|
552
|
-
};
|
|
553
|
-
const boundaries = [];
|
|
554
|
-
const visemes = [];
|
|
555
|
-
const bookmarks = [];
|
|
556
|
-
let sourceEventCursor = 0;
|
|
557
|
-
let generatedSourceMap;
|
|
558
|
-
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
559
|
-
try {
|
|
560
|
-
generatedSourceMap = getSsmlSourceMap(ssml);
|
|
561
|
-
} catch {
|
|
562
|
-
generatedSourceMap = void 0;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
566
|
-
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
567
|
-
...segment,
|
|
568
|
-
range: {
|
|
569
|
-
start: segment.range.start + sourceBaseOffset,
|
|
570
|
-
end: segment.range.end + sourceBaseOffset
|
|
571
|
-
},
|
|
572
|
-
sourceNodePath: [...segment.sourceNodePath]
|
|
573
|
-
})) ?? [];
|
|
574
|
-
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
575
|
-
...marker,
|
|
576
|
-
originalTextRange: {
|
|
577
|
-
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
578
|
-
end: marker.originalTextRange.end + sourceBaseOffset
|
|
579
|
-
},
|
|
580
|
-
sourceNodePath: [...marker.sourceNodePath]
|
|
581
|
-
})) ?? [];
|
|
582
|
-
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
583
|
-
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
584
|
-
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
585
|
-
if (marker) {
|
|
586
|
-
return {
|
|
587
|
-
originalTextRange: { ...marker.originalTextRange },
|
|
588
|
-
sourceNodePath: [...marker.sourceNodePath],
|
|
589
|
-
textRange: { ...marker.originalTextRange },
|
|
590
|
-
mappingStatus: "exact"
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
594
|
-
return { mappingStatus: "unmapped" };
|
|
595
|
-
}
|
|
596
|
-
const value = text ?? "";
|
|
597
|
-
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
598
|
-
let mappingStatus = "exact";
|
|
599
|
-
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
600
|
-
localStart = -1;
|
|
601
|
-
mappingStatus = "fallback";
|
|
602
|
-
}
|
|
603
|
-
if (localStart < 0 || localStart > sourceText.length) {
|
|
604
|
-
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
605
|
-
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
606
|
-
mappingStatus = "fallback";
|
|
607
|
-
}
|
|
608
|
-
localStart = Math.max(0, localStart);
|
|
609
|
-
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
610
|
-
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
611
|
-
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
612
|
-
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
613
|
-
const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
|
|
614
|
-
return {
|
|
615
|
-
originalTextRange: { ...fallbackRange },
|
|
616
|
-
textRange: { ...fallbackRange },
|
|
617
|
-
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
618
|
-
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
619
|
-
};
|
|
620
|
-
};
|
|
621
|
-
synthesizer.wordBoundary = (_sender, event) => {
|
|
622
|
-
boundaries.push({
|
|
623
|
-
text: event.text,
|
|
624
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
625
|
-
durationMs: ticksToMilliseconds(event.duration),
|
|
626
|
-
...mapSourceEvent(
|
|
627
|
-
event.text,
|
|
628
|
-
event.textOffset
|
|
629
|
-
)
|
|
630
|
-
});
|
|
631
|
-
};
|
|
632
|
-
synthesizer.visemeReceived = (_sender, event) => {
|
|
633
|
-
const eventWithOffset = event;
|
|
634
|
-
visemes.push({
|
|
635
|
-
visemeId: event.visemeId,
|
|
636
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
637
|
-
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
638
|
-
});
|
|
639
|
-
};
|
|
640
|
-
synthesizer.bookmarkReached = (_sender, event) => {
|
|
641
|
-
const eventWithOffset = event;
|
|
642
|
-
bookmarks.push({
|
|
643
|
-
name: event.text,
|
|
644
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
645
|
-
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
646
|
-
});
|
|
647
|
-
};
|
|
648
|
-
const cb = (result) => {
|
|
649
|
-
if (settled) return;
|
|
650
|
-
const { reason, errorDetails } = result;
|
|
651
|
-
if (reason !== SpeechSDK2.ResultReason.SynthesizingAudioCompleted) {
|
|
652
|
-
const err = errorDetails || `Speech synthesis failed with reason ${reason}.`;
|
|
653
|
-
rejectWithError(err);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
let audioSpec;
|
|
657
|
-
try {
|
|
658
|
-
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
659
|
-
} catch (error) {
|
|
660
|
-
rejectWithError(error);
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
settled = true;
|
|
664
|
-
cleanup();
|
|
665
|
-
closeResources();
|
|
666
|
-
const eventDurationMs = Math.max(
|
|
667
|
-
0,
|
|
668
|
-
...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
|
|
669
|
-
...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
|
|
670
|
-
...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
|
|
671
|
-
);
|
|
672
|
-
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
673
|
-
const requestId = result.resultId;
|
|
674
|
-
const addSourceMetadata = (event) => {
|
|
675
|
-
const mapped = {
|
|
676
|
-
...event,
|
|
677
|
-
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
678
|
-
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
679
|
-
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
680
|
-
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
681
|
-
...requestId ? { requestId } : {}
|
|
682
|
-
};
|
|
683
|
-
if (event.mappingStatus === "unmapped") {
|
|
684
|
-
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
685
|
-
Object.defineProperty(mapped, "toJSON", {
|
|
686
|
-
value: () => ({ ...mapped, mappingStatus: "unmapped" }),
|
|
687
|
-
enumerable: false
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
return mapped;
|
|
691
|
-
};
|
|
692
|
-
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
693
|
-
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
694
|
-
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
695
|
-
resolve({
|
|
696
|
-
audioData: result.audioData,
|
|
697
|
-
durationMs,
|
|
698
|
-
audioSpec,
|
|
699
|
-
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
700
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
701
|
-
...requestId ? { requestId } : {},
|
|
702
|
-
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
703
|
-
...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
|
|
704
|
-
...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
|
|
705
|
-
});
|
|
706
|
-
};
|
|
707
|
-
try {
|
|
708
|
-
if (config.signal) {
|
|
709
|
-
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
710
|
-
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
711
|
-
}
|
|
712
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
713
|
-
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
714
|
-
timeout = setTimeout(
|
|
715
|
-
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
716
|
-
timeoutMs
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
720
|
-
} catch (error) {
|
|
721
|
-
rejectWithError(error);
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
function isRetryableSynthesisError(error) {
|
|
726
|
-
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
727
|
-
if (error instanceof AzureTtsError && error.status !== 0)
|
|
728
|
-
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
729
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
730
|
-
if (/\b4\d{2}\b/.test(message)) return false;
|
|
731
|
-
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
732
|
-
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
733
|
-
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
734
|
-
}
|
|
735
|
-
function retryDelay(options, retryAttempt, error) {
|
|
736
|
-
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
737
|
-
if (retryAfterMs !== void 0) return retryAfterMs;
|
|
738
|
-
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
739
|
-
return Math.floor(Math.random() * (base + 1));
|
|
740
|
-
}
|
|
741
|
-
function resolveConcurrency(value, total) {
|
|
742
|
-
if (value === void 0) return 1;
|
|
743
|
-
if (value === Infinity) return Math.max(1, total);
|
|
744
|
-
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
745
|
-
}
|
|
746
|
-
async function waitForRetry(delayMs, signal) {
|
|
747
|
-
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
748
|
-
if (delayMs <= 0) return;
|
|
749
|
-
await new Promise((resolve, reject) => {
|
|
750
|
-
let timer;
|
|
751
|
-
const abort = () => {
|
|
752
|
-
clearTimeout(timer);
|
|
753
|
-
signal?.removeEventListener("abort", abort);
|
|
754
|
-
reject(new SynthesisCancelledError());
|
|
755
|
-
};
|
|
756
|
-
timer = setTimeout(() => {
|
|
757
|
-
signal?.removeEventListener("abort", abort);
|
|
758
|
-
resolve();
|
|
759
|
-
}, delayMs);
|
|
760
|
-
if (signal) {
|
|
761
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
766
|
-
const options = retryOptions ? {
|
|
767
|
-
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
768
|
-
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
769
|
-
maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
|
|
770
|
-
shouldRetry: retryOptions.shouldRetry
|
|
771
|
-
} : void 0;
|
|
772
|
-
let attempt = 0;
|
|
773
|
-
while (true) {
|
|
774
|
-
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
775
|
-
try {
|
|
776
|
-
return await synthesizeSsmlOnce(ssml, config);
|
|
777
|
-
} catch (error) {
|
|
778
|
-
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
779
|
-
throw error;
|
|
780
|
-
attempt += 1;
|
|
781
|
-
const delayMs = retryDelay(options, attempt, error);
|
|
782
|
-
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
783
|
-
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
784
|
-
throw new SynthesisTimeoutError(
|
|
785
|
-
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
onRetry(attempt, delayMs);
|
|
789
|
-
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
async function synthesizeSsml(ssml, config) {
|
|
794
|
-
const totalJobMs = config.timeouts?.totalJobMs;
|
|
795
|
-
const deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
796
|
-
if (!config.retryOptions) return synthesizeSsmlOnce(ssml, config);
|
|
797
|
-
return synthesizeWithRetry(ssml, config, config.retryOptions, () => void 0, deadlineAtMs);
|
|
798
|
-
}
|
|
799
|
-
function createAbortScope(parent, timeoutMs) {
|
|
800
|
-
const controller = new AbortController();
|
|
801
|
-
let didTimeout = false;
|
|
802
|
-
const onAbort = () => controller.abort();
|
|
803
|
-
if (parent?.aborted) controller.abort();
|
|
804
|
-
parent?.addEventListener("abort", onAbort, { once: true });
|
|
805
|
-
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
806
|
-
didTimeout = true;
|
|
807
|
-
controller.abort();
|
|
808
|
-
}, timeoutMs) : void 0;
|
|
809
|
-
return {
|
|
810
|
-
signal: controller.signal,
|
|
811
|
-
timedOut: () => didTimeout,
|
|
812
|
-
dispose: () => {
|
|
813
|
-
if (timer) clearTimeout(timer);
|
|
814
|
-
parent?.removeEventListener("abort", onAbort);
|
|
815
|
-
},
|
|
816
|
-
abort: () => controller.abort()
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
820
|
-
const scope = createAbortScope(config.signal, timeoutMs);
|
|
821
|
-
try {
|
|
822
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
823
|
-
} catch (error) {
|
|
824
|
-
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
825
|
-
throw error;
|
|
826
|
-
} finally {
|
|
827
|
-
scope.dispose();
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
async function synthesizeSsmlChunks(chunks, config) {
|
|
831
|
-
const totalChunks = chunks.length;
|
|
832
|
-
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
833
|
-
const fingerprints = inputs.map(
|
|
834
|
-
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT)
|
|
835
|
-
);
|
|
836
|
-
const results = new Array(totalChunks);
|
|
837
|
-
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
838
|
-
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
839
|
-
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
840
|
-
chunkIndex,
|
|
841
|
-
status: "pending",
|
|
842
|
-
canResume: true
|
|
843
|
-
}));
|
|
844
|
-
for (const [index, cached] of cachedChunks) {
|
|
845
|
-
if (index < 0 || index >= totalChunks) continue;
|
|
846
|
-
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
847
|
-
if (isValid) {
|
|
848
|
-
results[index] = { ...cached };
|
|
849
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
850
|
-
} else {
|
|
851
|
-
invalidCachedIndices.add(index);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
855
|
-
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
856
|
-
const jobStartedAt = Date.now();
|
|
857
|
-
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
858
|
-
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
859
|
-
const report = (event) => config.onProgress?.(event);
|
|
860
|
-
for (const [index, input] of inputs.entries()) {
|
|
861
|
-
report({
|
|
862
|
-
currentChunk: index,
|
|
863
|
-
totalChunks,
|
|
864
|
-
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
865
|
-
chunkIndex: index,
|
|
866
|
-
originalTextRange: input.originalTextRange,
|
|
867
|
-
status: "pending",
|
|
868
|
-
durationMs: 0
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
let completed = [...results].filter((result) => result !== void 0).length;
|
|
872
|
-
let nextIndex = 0;
|
|
873
|
-
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
874
|
-
let firstError;
|
|
875
|
-
const failedIndices = /* @__PURE__ */ new Set();
|
|
876
|
-
const worker = async () => {
|
|
877
|
-
while (true) {
|
|
878
|
-
const index = nextIndex++;
|
|
879
|
-
if (index >= chunks.length) return;
|
|
880
|
-
if (!shouldSynthesize(index)) continue;
|
|
881
|
-
if (firstError && config.cancelOnFailure !== false) return;
|
|
882
|
-
const input = inputs[index];
|
|
883
|
-
report({
|
|
884
|
-
currentChunk: completed,
|
|
885
|
-
totalChunks,
|
|
886
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
887
|
-
chunkIndex: index,
|
|
888
|
-
originalTextRange: input.originalTextRange,
|
|
889
|
-
status: "synthesizing",
|
|
890
|
-
durationMs: 0
|
|
891
|
-
});
|
|
892
|
-
const startedAt = Date.now();
|
|
893
|
-
try {
|
|
894
|
-
const result = await synthesizeChunkWithTimeout(
|
|
895
|
-
input.ssml,
|
|
896
|
-
{
|
|
897
|
-
...config,
|
|
898
|
-
signal: scope.signal,
|
|
899
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
900
|
-
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
901
|
-
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
902
|
-
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
903
|
-
chunkIndex: index,
|
|
904
|
-
onProgress: void 0
|
|
905
|
-
},
|
|
906
|
-
config.retryOptions,
|
|
907
|
-
config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
|
|
908
|
-
(retryAttempt, nextRetryDelayMs) => report({
|
|
909
|
-
currentChunk: completed,
|
|
910
|
-
totalChunks,
|
|
911
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
912
|
-
chunkIndex: index,
|
|
913
|
-
originalTextRange: input.originalTextRange,
|
|
914
|
-
status: "synthesizing",
|
|
915
|
-
durationMs: Date.now() - startedAt,
|
|
916
|
-
retryAttempt,
|
|
917
|
-
nextRetryDelayMs,
|
|
918
|
-
isRetrying: true
|
|
919
|
-
}),
|
|
920
|
-
jobDeadlineAt
|
|
921
|
-
);
|
|
922
|
-
results[index] = result;
|
|
923
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
924
|
-
completed += 1;
|
|
925
|
-
report({
|
|
926
|
-
currentChunk: completed,
|
|
927
|
-
totalChunks,
|
|
928
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
929
|
-
chunkIndex: index,
|
|
930
|
-
originalTextRange: input.originalTextRange,
|
|
931
|
-
status: "success",
|
|
932
|
-
durationMs: Date.now() - startedAt
|
|
933
|
-
});
|
|
934
|
-
} catch (error) {
|
|
935
|
-
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
936
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
937
|
-
if (!wasCancelled) failedIndices.add(index);
|
|
938
|
-
chunkStates[index] = {
|
|
939
|
-
chunkIndex: index,
|
|
940
|
-
status: wasCancelled ? "cancelled" : "failed",
|
|
941
|
-
isOriginalFailure: !wasCancelled,
|
|
942
|
-
canResume: true,
|
|
943
|
-
error
|
|
944
|
-
};
|
|
945
|
-
report({
|
|
946
|
-
currentChunk: completed,
|
|
947
|
-
totalChunks,
|
|
948
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
949
|
-
chunkIndex: index,
|
|
950
|
-
originalTextRange: input.originalTextRange,
|
|
951
|
-
status: "failed",
|
|
952
|
-
durationMs: Date.now() - startedAt,
|
|
953
|
-
error
|
|
954
|
-
});
|
|
955
|
-
if (config.cancelOnFailure !== false) scope.abort();
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
};
|
|
960
|
-
try {
|
|
961
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
962
|
-
if (firstError) throw firstError;
|
|
963
|
-
const orderedResults = results.filter((result) => result !== void 0);
|
|
964
|
-
return await mergeSynthesisResults(orderedResults, {
|
|
965
|
-
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
966
|
-
signal: scope.signal,
|
|
967
|
-
customMerger: config.customMerger,
|
|
968
|
-
outputMimeType: config.outputMimeType,
|
|
969
|
-
postMergeValidator: config.postMergeValidator
|
|
970
|
-
});
|
|
971
|
-
} catch (error) {
|
|
972
|
-
if (firstError && config.cancelOnFailure !== false) {
|
|
973
|
-
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
974
|
-
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
975
|
-
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
const synthesizedChunks = results.flatMap(
|
|
980
|
-
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
981
|
-
);
|
|
982
|
-
const partial = {
|
|
983
|
-
synthesizedChunks,
|
|
984
|
-
completedChunks: synthesizedChunks,
|
|
985
|
-
pendingChunkIndices: chunkStates.flatMap(
|
|
986
|
-
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
987
|
-
),
|
|
988
|
-
failedChunkIndices: [...failedIndices],
|
|
989
|
-
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
990
|
-
chunkStates,
|
|
991
|
-
totalChunks
|
|
992
|
-
};
|
|
993
|
-
if (error && typeof error === "object") error.partialResult = partial;
|
|
994
|
-
throw error;
|
|
995
|
-
} finally {
|
|
996
|
-
scope.dispose();
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
1000
|
-
const boundaries = [];
|
|
1001
|
-
const visemes = [];
|
|
1002
|
-
const bookmarks = [];
|
|
1003
|
-
let durationOffset = 0;
|
|
1004
|
-
for (const [resultIndex, result] of results.entries()) {
|
|
1005
|
-
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
1006
|
-
for (const boundary of chunkBoundaries) {
|
|
1007
|
-
const textRange = boundary.textRange ?? result.textRange;
|
|
1008
|
-
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
1009
|
-
const requestId = boundary.requestId ?? result.requestId;
|
|
1010
|
-
boundaries.push({
|
|
1011
|
-
...boundary,
|
|
1012
|
-
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
1013
|
-
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
1014
|
-
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1015
|
-
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
1016
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1017
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1018
|
-
...requestId ? { requestId } : {},
|
|
1019
|
-
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
for (const viseme of result.visemes ?? []) {
|
|
1023
|
-
const textRange = viseme.textRange ?? result.textRange;
|
|
1024
|
-
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
1025
|
-
const requestId = viseme.requestId ?? result.requestId;
|
|
1026
|
-
visemes.push({
|
|
1027
|
-
...viseme,
|
|
1028
|
-
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
1029
|
-
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
1030
|
-
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1031
|
-
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
1032
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1033
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1034
|
-
...requestId ? { requestId } : {},
|
|
1035
|
-
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
for (const bookmark of result.bookmarks ?? []) {
|
|
1039
|
-
const textRange = bookmark.textRange ?? result.textRange;
|
|
1040
|
-
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
1041
|
-
const requestId = bookmark.requestId ?? result.requestId;
|
|
1042
|
-
bookmarks.push({
|
|
1043
|
-
...bookmark,
|
|
1044
|
-
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
1045
|
-
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
1046
|
-
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1047
|
-
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
1048
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1049
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1050
|
-
...requestId ? { requestId } : {},
|
|
1051
|
-
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
durationOffset += Math.max(0, result.durationMs);
|
|
1055
|
-
}
|
|
1056
|
-
return {
|
|
1057
|
-
audioData,
|
|
1058
|
-
durationMs: durationOffset,
|
|
1059
|
-
mimeType: resolveMimeType(format),
|
|
1060
|
-
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
1061
|
-
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
1062
|
-
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
1063
|
-
...visemes.length > 0 ? { visemes } : {},
|
|
1064
|
-
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
1065
|
-
...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
|
|
1066
|
-
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
function mergeSynthesisResults(results, options) {
|
|
1070
|
-
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
1071
|
-
const format = resolvedOptions?.format;
|
|
1072
|
-
if (!format) throw new UnsupportedMergeFormatError("");
|
|
1073
|
-
const buffers = results.map((result) => result.audioData);
|
|
1074
|
-
const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
|
|
1075
|
-
validateAudioSpecifications(inputSpecs);
|
|
1076
|
-
const signal = resolvedOptions.signal ?? new AbortController().signal;
|
|
1077
|
-
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1078
|
-
if (resolvedOptions.customMerger) {
|
|
1079
|
-
return Promise.resolve().then(
|
|
1080
|
-
() => resolvedOptions.customMerger?.(buffers, {
|
|
1081
|
-
format,
|
|
1082
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1083
|
-
inputSpecs,
|
|
1084
|
-
signal
|
|
1085
|
-
})
|
|
1086
|
-
).then((merged) => {
|
|
1087
|
-
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
1088
|
-
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1089
|
-
const mergedSpec = validateMergedAudioBuffer(
|
|
1090
|
-
merged,
|
|
1091
|
-
format,
|
|
1092
|
-
buffers,
|
|
1093
|
-
inputSpecs,
|
|
1094
|
-
resolvedOptions.outputMimeType ?? resolveMimeType(format)
|
|
1095
|
-
);
|
|
1096
|
-
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
1097
|
-
return Promise.resolve(
|
|
1098
|
-
resolvedOptions.postMergeValidator?.(result, {
|
|
1099
|
-
format,
|
|
1100
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1101
|
-
inputSpecs,
|
|
1102
|
-
signal
|
|
1103
|
-
})
|
|
1104
|
-
).then((valid) => {
|
|
1105
|
-
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1106
|
-
return result;
|
|
1107
|
-
});
|
|
1108
|
-
}).catch((error) => {
|
|
1109
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1110
|
-
throw error;
|
|
1111
|
-
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
try {
|
|
1115
|
-
const result = createMergedResult(
|
|
1116
|
-
results,
|
|
1117
|
-
mergeAudioBuffers(buffers, { format }),
|
|
1118
|
-
format,
|
|
1119
|
-
inputSpecs[0],
|
|
1120
|
-
resolvedOptions.outputMimeType
|
|
1121
|
-
);
|
|
1122
|
-
if (resolvedOptions.postMergeValidator) {
|
|
1123
|
-
const validation = resolvedOptions.postMergeValidator(result, {
|
|
1124
|
-
format,
|
|
1125
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1126
|
-
inputSpecs,
|
|
1127
|
-
signal
|
|
1128
|
-
});
|
|
1129
|
-
if (validation instanceof Promise)
|
|
1130
|
-
return validation.then((valid) => {
|
|
1131
|
-
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1132
|
-
return result;
|
|
1133
|
-
});
|
|
1134
|
-
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1135
|
-
}
|
|
1136
|
-
return result;
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1139
|
-
throw error;
|
|
1140
|
-
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
async function synthesizeSpeech(ssml, config) {
|
|
1144
|
-
return (await synthesizeSsml(ssml, config)).audioData;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// src/safe.ts
|
|
1148
|
-
import {
|
|
1149
|
-
createAzureUrlValidatorRunner,
|
|
1150
|
-
validateAzureSsml
|
|
1151
|
-
} from "@ssml-builder-js/ssml-core";
|
|
1152
|
-
var ChunkValidationError = class extends Error {
|
|
1153
|
-
constructor(chunkIndex, diagnostics) {
|
|
1154
|
-
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
1155
|
-
this.kind = "validation-error";
|
|
1156
|
-
this.name = "ChunkValidationError";
|
|
1157
|
-
this.chunkIndex = chunkIndex;
|
|
1158
|
-
this.diagnostics = diagnostics;
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
var BatchChunkValidationError = class extends ChunkValidationError {
|
|
1162
|
-
constructor(chunkDiagnostics) {
|
|
1163
|
-
const first = chunkDiagnostics[0];
|
|
1164
|
-
super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
|
|
1165
|
-
this.name = "BatchChunkValidationError";
|
|
1166
|
-
this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
|
|
1167
|
-
this.chunkDiagnostics = chunkDiagnostics;
|
|
1168
|
-
this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
|
|
1169
|
-
this.errorCount = this.totalErrorCount;
|
|
1170
|
-
this.totalErrors = this.totalErrorCount;
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
function failure(error, partialResult) {
|
|
1174
|
-
return {
|
|
1175
|
-
ok: false,
|
|
1176
|
-
success: false,
|
|
1177
|
-
status: error.kind,
|
|
1178
|
-
error,
|
|
1179
|
-
...partialResult ? { partialResult } : {}
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
function partialResultFrom(error) {
|
|
1183
|
-
if (!error || typeof error !== "object") return void 0;
|
|
1184
|
-
const partial = error.partialResult;
|
|
1185
|
-
if (!partial || typeof partial !== "object") return void 0;
|
|
1186
|
-
return partial;
|
|
1187
|
-
}
|
|
1188
|
-
function createSafeAbortScope(parent, timeoutMs) {
|
|
1189
|
-
const controller = new AbortController();
|
|
1190
|
-
let didTimeout = false;
|
|
1191
|
-
const onAbort = () => controller.abort();
|
|
1192
|
-
if (parent?.aborted) controller.abort();
|
|
1193
|
-
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1194
|
-
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
1195
|
-
didTimeout = true;
|
|
1196
|
-
controller.abort();
|
|
1197
|
-
}, timeoutMs) : void 0;
|
|
1198
|
-
return {
|
|
1199
|
-
signal: controller.signal,
|
|
1200
|
-
timedOut: () => didTimeout,
|
|
1201
|
-
dispose: () => {
|
|
1202
|
-
if (timer) clearTimeout(timer);
|
|
1203
|
-
parent?.removeEventListener("abort", onAbort);
|
|
1204
|
-
},
|
|
1205
|
-
abort: () => controller.abort()
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
function isRetryable(error) {
|
|
1209
|
-
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
1210
|
-
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
1211
|
-
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
1212
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1213
|
-
if (/\b4\d{2}\b/.test(message)) return false;
|
|
1214
|
-
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
1215
|
-
}
|
|
1216
|
-
function delayForRetry(options, attempt) {
|
|
1217
|
-
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
1218
|
-
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
1219
|
-
return Math.floor(Math.random() * (base + 1));
|
|
1220
|
-
}
|
|
1221
|
-
function retryDelayForError(options, attempt, error) {
|
|
1222
|
-
return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
|
|
1223
|
-
}
|
|
1224
|
-
function resolveConcurrency2(value, total) {
|
|
1225
|
-
if (value === void 0) return 1;
|
|
1226
|
-
if (value === Infinity) return Math.max(1, total);
|
|
1227
|
-
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1228
|
-
}
|
|
1229
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1230
|
-
const retry = options ? {
|
|
1231
|
-
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1232
|
-
initialDelayMs: options.initialDelayMs,
|
|
1233
|
-
maxDelayMs: options.maxDelayMs,
|
|
1234
|
-
shouldRetry: options.shouldRetry
|
|
1235
|
-
} : void 0;
|
|
1236
|
-
let attempt = 0;
|
|
1237
|
-
while (true) {
|
|
1238
|
-
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
1239
|
-
try {
|
|
1240
|
-
return await synthesize();
|
|
1241
|
-
} catch (error) {
|
|
1242
|
-
if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
|
|
1243
|
-
throw error;
|
|
1244
|
-
attempt += 1;
|
|
1245
|
-
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1246
|
-
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1247
|
-
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1248
|
-
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1249
|
-
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1250
|
-
}
|
|
1251
|
-
onRetry(attempt, delayMs);
|
|
1252
|
-
if (delayMs > 0)
|
|
1253
|
-
await new Promise((resolve, reject) => {
|
|
1254
|
-
const timer = setTimeout(() => {
|
|
1255
|
-
signal?.removeEventListener("abort", abort);
|
|
1256
|
-
resolve();
|
|
1257
|
-
}, delayMs);
|
|
1258
|
-
const abort = () => {
|
|
1259
|
-
clearTimeout(timer);
|
|
1260
|
-
signal?.removeEventListener("abort", abort);
|
|
1261
|
-
reject(new Error("Speech synthesis was cancelled."));
|
|
1262
|
-
};
|
|
1263
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
function sharedValidationOptions(options, signal) {
|
|
1269
|
-
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1270
|
-
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
1271
|
-
const runner = createAzureUrlValidatorRunner(validator, {
|
|
1272
|
-
...options.urlValidation ?? {},
|
|
1273
|
-
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1274
|
-
...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1275
|
-
...signal ? { signal } : {},
|
|
1276
|
-
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1277
|
-
});
|
|
1278
|
-
return {
|
|
1279
|
-
...withValidationSignal(options, signal),
|
|
1280
|
-
urlValidatorRunner: runner
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1284
|
-
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
1285
|
-
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
1286
|
-
if (options.signal?.aborted) {
|
|
1287
|
-
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1288
|
-
return failure(error);
|
|
1289
|
-
}
|
|
1290
|
-
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1291
|
-
if (errors.length > 0) {
|
|
1292
|
-
return failure({
|
|
1293
|
-
kind: "validation-error",
|
|
1294
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
1295
|
-
diagnostics: errors
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
const jobScope = options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts.totalJobMs) : void 0;
|
|
1299
|
-
try {
|
|
1300
|
-
return {
|
|
1301
|
-
ok: true,
|
|
1302
|
-
success: true,
|
|
1303
|
-
status: "success",
|
|
1304
|
-
value: await client.synthesizeSsml(ssml, {
|
|
1305
|
-
signal: jobScope?.signal ?? options.signal,
|
|
1306
|
-
timeoutMs: options.timeouts?.perChunkMs,
|
|
1307
|
-
timeouts: options.timeouts
|
|
1308
|
-
})
|
|
1309
|
-
};
|
|
1310
|
-
} catch (error) {
|
|
1311
|
-
if (jobScope?.timedOut()) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1312
|
-
const synthesisError = toSynthesisError(error);
|
|
1313
|
-
return failure(synthesisError);
|
|
1314
|
-
} finally {
|
|
1315
|
-
jobScope?.dispose();
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1319
|
-
const validationOptions = sharedValidationOptions(
|
|
1320
|
-
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1321
|
-
options.signal
|
|
1322
|
-
);
|
|
1323
|
-
if (options.signal?.aborted) {
|
|
1324
|
-
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1325
|
-
return failure(error);
|
|
1326
|
-
}
|
|
1327
|
-
const pending = (index, status, error) => {
|
|
1328
|
-
options.onProgress?.({
|
|
1329
|
-
currentChunk: status === "success" ? index + 1 : index,
|
|
1330
|
-
totalChunks: chunks.length,
|
|
1331
|
-
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
1332
|
-
chunkIndex: index,
|
|
1333
|
-
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
1334
|
-
status,
|
|
1335
|
-
durationMs: 0,
|
|
1336
|
-
...error ? { error } : {}
|
|
1337
|
-
});
|
|
1338
|
-
};
|
|
1339
|
-
chunks.forEach((_chunk, index) => {
|
|
1340
|
-
pending(index, "pending");
|
|
1341
|
-
});
|
|
1342
|
-
const validations = await Promise.all(
|
|
1343
|
-
chunks.map(async (chunk, index) => {
|
|
1344
|
-
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
1345
|
-
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
1346
|
-
const diagnostics = await Promise.resolve(
|
|
1347
|
-
validateAzureSsml(ssml, {
|
|
1348
|
-
...validationOptions,
|
|
1349
|
-
...sourceNodePath ? { sourceNodePath } : {},
|
|
1350
|
-
chunkIndex: index
|
|
1351
|
-
})
|
|
1352
|
-
);
|
|
1353
|
-
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1354
|
-
})
|
|
1355
|
-
);
|
|
1356
|
-
if (options.signal?.aborted) {
|
|
1357
|
-
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1358
|
-
return failure(error);
|
|
1359
|
-
}
|
|
1360
|
-
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1361
|
-
if (chunkDiagnostics.length > 0) {
|
|
1362
|
-
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1363
|
-
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1364
|
-
return failure(error);
|
|
1365
|
-
}
|
|
1366
|
-
let fallbackJobScope;
|
|
1367
|
-
try {
|
|
1368
|
-
if (client.synthesizeChunks) {
|
|
1369
|
-
const normalizedChunks = chunks.map((chunk) => {
|
|
1370
|
-
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
1371
|
-
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
1372
|
-
});
|
|
1373
|
-
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1374
|
-
onProgress: options.onProgress,
|
|
1375
|
-
outputFormat: options.outputFormat,
|
|
1376
|
-
signal: options.signal,
|
|
1377
|
-
timeoutMs: options.timeoutMs,
|
|
1378
|
-
timeouts: options.timeouts,
|
|
1379
|
-
sourceNodePath: options.sourceNodePath,
|
|
1380
|
-
concurrency: options.concurrency,
|
|
1381
|
-
retryOptions: options.retryOptions,
|
|
1382
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
1383
|
-
resumeChunks: options.resumeChunks,
|
|
1384
|
-
resumeChunkIndices: options.resumeChunkIndices,
|
|
1385
|
-
customMerger: options.customMerger,
|
|
1386
|
-
outputMimeType: options.outputMimeType,
|
|
1387
|
-
postMergeValidator: options.postMergeValidator,
|
|
1388
|
-
resumeValidation: options.resumeValidation
|
|
1389
|
-
});
|
|
1390
|
-
return { ok: true, success: true, status: "success", value };
|
|
1391
|
-
}
|
|
1392
|
-
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1393
|
-
const fingerprints = inputs.map((chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat));
|
|
1394
|
-
const results = new Array(chunks.length);
|
|
1395
|
-
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1396
|
-
chunkIndex,
|
|
1397
|
-
status: "pending",
|
|
1398
|
-
canResume: true
|
|
1399
|
-
}));
|
|
1400
|
-
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1401
|
-
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1402
|
-
for (const [index, cached] of cachedChunks) {
|
|
1403
|
-
if (index < 0 || index >= chunks.length) continue;
|
|
1404
|
-
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1405
|
-
results[index] = cached;
|
|
1406
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1407
|
-
} else invalidCachedIndices.add(index);
|
|
1408
|
-
}
|
|
1409
|
-
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1410
|
-
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1411
|
-
const jobStartedAt = Date.now();
|
|
1412
|
-
const jobDeadlineAt = options.timeouts?.totalJobMs !== void 0 && options.timeouts.totalJobMs > 0 ? jobStartedAt + options.timeouts.totalJobMs : void 0;
|
|
1413
|
-
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1414
|
-
fallbackJobScope = jobScope;
|
|
1415
|
-
const failedIndices = /* @__PURE__ */ new Set();
|
|
1416
|
-
let firstError;
|
|
1417
|
-
let completed = [...results].filter((result) => result !== void 0).length;
|
|
1418
|
-
let nextIndex = 0;
|
|
1419
|
-
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1420
|
-
const worker = async () => {
|
|
1421
|
-
while (true) {
|
|
1422
|
-
const index = nextIndex++;
|
|
1423
|
-
if (index >= chunks.length) return;
|
|
1424
|
-
if (!shouldSynthesize(index)) continue;
|
|
1425
|
-
if (firstError && options.cancelOnFailure !== false) {
|
|
1426
|
-
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
const chunk = chunks[index];
|
|
1430
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1431
|
-
const sourceNodePath = input.sourceNodePath;
|
|
1432
|
-
const originalTextRange = input.originalTextRange;
|
|
1433
|
-
pending(index, "synthesizing");
|
|
1434
|
-
const startedAt = Date.now();
|
|
1435
|
-
try {
|
|
1436
|
-
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1437
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? options.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1438
|
-
const chunkSignal = chunkScope?.signal ?? options.signal;
|
|
1439
|
-
let result;
|
|
1440
|
-
try {
|
|
1441
|
-
result = await retryableSynthesis(
|
|
1442
|
-
() => client.synthesizeSsml(input.ssml, {
|
|
1443
|
-
outputFormat: options.outputFormat,
|
|
1444
|
-
signal: chunkSignal,
|
|
1445
|
-
timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
|
|
1446
|
-
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1447
|
-
}),
|
|
1448
|
-
options.retryOptions,
|
|
1449
|
-
chunkSignal,
|
|
1450
|
-
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1451
|
-
currentChunk: completed,
|
|
1452
|
-
totalChunks: chunks.length,
|
|
1453
|
-
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1454
|
-
chunkIndex: index,
|
|
1455
|
-
originalTextRange: input.originalTextRange,
|
|
1456
|
-
status: "synthesizing",
|
|
1457
|
-
durationMs: Date.now() - startedAt,
|
|
1458
|
-
retryAttempt,
|
|
1459
|
-
nextRetryDelayMs,
|
|
1460
|
-
isRetrying: true
|
|
1461
|
-
}),
|
|
1462
|
-
jobDeadlineAt
|
|
1463
|
-
);
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
if (chunkScope?.timedOut())
|
|
1466
|
-
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1467
|
-
throw error;
|
|
1468
|
-
} finally {
|
|
1469
|
-
chunkScope?.dispose();
|
|
1470
|
-
}
|
|
1471
|
-
results[index] = {
|
|
1472
|
-
...result,
|
|
1473
|
-
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1474
|
-
...sourceNodePath ? {
|
|
1475
|
-
boundaries: result.boundaries?.map((event) => ({
|
|
1476
|
-
...event,
|
|
1477
|
-
sourceNodePath: [...sourceNodePath],
|
|
1478
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1479
|
-
})),
|
|
1480
|
-
visemes: result.visemes?.map((event) => ({
|
|
1481
|
-
...event,
|
|
1482
|
-
sourceNodePath: [...sourceNodePath],
|
|
1483
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1484
|
-
})),
|
|
1485
|
-
bookmarks: result.bookmarks?.map((event) => ({
|
|
1486
|
-
...event,
|
|
1487
|
-
sourceNodePath: [...sourceNodePath],
|
|
1488
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1489
|
-
}))
|
|
1490
|
-
} : {},
|
|
1491
|
-
...originalTextRange ? {
|
|
1492
|
-
boundaries: result.boundaries?.map((event) => ({
|
|
1493
|
-
...event,
|
|
1494
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1495
|
-
})),
|
|
1496
|
-
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1497
|
-
...event,
|
|
1498
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1499
|
-
})),
|
|
1500
|
-
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1501
|
-
...event,
|
|
1502
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1503
|
-
})),
|
|
1504
|
-
visemes: result.visemes?.map((event) => ({
|
|
1505
|
-
...event,
|
|
1506
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1507
|
-
})),
|
|
1508
|
-
bookmarks: result.bookmarks?.map((event) => ({
|
|
1509
|
-
...event,
|
|
1510
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1511
|
-
}))
|
|
1512
|
-
} : {}
|
|
1513
|
-
};
|
|
1514
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1515
|
-
completed += 1;
|
|
1516
|
-
options.onProgress?.({
|
|
1517
|
-
currentChunk: completed,
|
|
1518
|
-
totalChunks: chunks.length,
|
|
1519
|
-
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1520
|
-
chunkIndex: index,
|
|
1521
|
-
originalTextRange: input.originalTextRange,
|
|
1522
|
-
status: "success",
|
|
1523
|
-
durationMs: Date.now() - startedAt
|
|
1524
|
-
});
|
|
1525
|
-
} catch (error) {
|
|
1526
|
-
const wasCancelled = firstError !== void 0 || Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1527
|
-
firstError ?? (firstError = error);
|
|
1528
|
-
if (!wasCancelled) failedIndices.add(index);
|
|
1529
|
-
chunkStates[index] = {
|
|
1530
|
-
chunkIndex: index,
|
|
1531
|
-
status: wasCancelled ? "cancelled" : "failed",
|
|
1532
|
-
isOriginalFailure: !wasCancelled,
|
|
1533
|
-
canResume: true,
|
|
1534
|
-
error
|
|
1535
|
-
};
|
|
1536
|
-
options.onProgress?.({
|
|
1537
|
-
currentChunk: completed,
|
|
1538
|
-
totalChunks: chunks.length,
|
|
1539
|
-
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1540
|
-
chunkIndex: index,
|
|
1541
|
-
originalTextRange: input.originalTextRange,
|
|
1542
|
-
status: "failed",
|
|
1543
|
-
durationMs: Date.now() - startedAt,
|
|
1544
|
-
error
|
|
1545
|
-
});
|
|
1546
|
-
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
};
|
|
1551
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1552
|
-
if (firstError && options.cancelOnFailure !== false) {
|
|
1553
|
-
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1554
|
-
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1555
|
-
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
if (failedIndices.size > 0) {
|
|
1560
|
-
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1561
|
-
const synthesizedChunks = results.flatMap(
|
|
1562
|
-
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1563
|
-
);
|
|
1564
|
-
error.partialResult = {
|
|
1565
|
-
synthesizedChunks,
|
|
1566
|
-
completedChunks: synthesizedChunks,
|
|
1567
|
-
pendingChunkIndices: chunkStates.flatMap(
|
|
1568
|
-
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1569
|
-
),
|
|
1570
|
-
failedChunkIndices: [...failedIndices],
|
|
1571
|
-
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1572
|
-
chunkStates,
|
|
1573
|
-
totalChunks: chunks.length
|
|
1574
|
-
};
|
|
1575
|
-
throw error;
|
|
1576
|
-
}
|
|
1577
|
-
const orderedResults = results.filter((result) => result !== void 0);
|
|
1578
|
-
return {
|
|
1579
|
-
ok: true,
|
|
1580
|
-
success: true,
|
|
1581
|
-
status: "success",
|
|
1582
|
-
value: await mergeSynthesisResults(orderedResults, {
|
|
1583
|
-
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1584
|
-
signal: jobScope?.signal ?? options.signal,
|
|
1585
|
-
customMerger: options.customMerger,
|
|
1586
|
-
outputMimeType: options.outputMimeType,
|
|
1587
|
-
postMergeValidator: options.postMergeValidator
|
|
1588
|
-
})
|
|
1589
|
-
};
|
|
1590
|
-
} catch (error) {
|
|
1591
|
-
const synthesisError = toSynthesisError(error);
|
|
1592
|
-
return failure(synthesisError, partialResultFrom(error));
|
|
1593
|
-
} finally {
|
|
1594
|
-
fallbackJobScope?.dispose();
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function withValidationSignal(options, signal) {
|
|
1598
|
-
if (!signal) return options;
|
|
1599
|
-
return {
|
|
1600
|
-
...options,
|
|
1601
|
-
urlValidatorSignal: signal,
|
|
1602
|
-
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// src/client.ts
|
|
1607
|
-
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
1608
|
-
var _options;
|
|
1609
|
-
var AzureTtsClient = class {
|
|
1610
|
-
constructor(options) {
|
|
1611
|
-
__privateAdd(this, _options);
|
|
1612
|
-
__privateSet(this, _options, options);
|
|
1613
|
-
}
|
|
1614
|
-
async synthesize(ssml) {
|
|
1615
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
1616
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1617
|
-
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1618
|
-
const config = {
|
|
1619
|
-
endpoint,
|
|
1620
|
-
region,
|
|
1621
|
-
subscriptionKey,
|
|
1622
|
-
outputFormat,
|
|
1623
|
-
signal,
|
|
1624
|
-
timeoutMs,
|
|
1625
|
-
timeouts,
|
|
1626
|
-
retryOptions: __privateGet(this, _options).retryOptions
|
|
1627
|
-
};
|
|
1628
|
-
return synthesizeSpeech(ssml, config);
|
|
1629
|
-
}
|
|
1630
|
-
async synthesizeSsml(ssml, options = {}) {
|
|
1631
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
1632
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1633
|
-
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1634
|
-
return synthesizeSsml(ssml, {
|
|
1635
|
-
endpoint,
|
|
1636
|
-
region,
|
|
1637
|
-
subscriptionKey,
|
|
1638
|
-
outputFormat: options.outputFormat ?? outputFormat,
|
|
1639
|
-
signal: options.signal ?? signal,
|
|
1640
|
-
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1641
|
-
timeouts: options.timeouts ?? timeouts,
|
|
1642
|
-
sourceNodePath: options.sourceNodePath,
|
|
1643
|
-
sourceTextSegments: options.sourceTextSegments,
|
|
1644
|
-
sourceMarkers: options.sourceMarkers,
|
|
1645
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1646
|
-
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1647
|
-
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1648
|
-
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1649
|
-
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1650
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
async synthesizeChunks(chunks, options = {}) {
|
|
1654
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
1655
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1656
|
-
return synthesizeSsmlChunks(chunks, {
|
|
1657
|
-
endpoint,
|
|
1658
|
-
region,
|
|
1659
|
-
subscriptionKey,
|
|
1660
|
-
outputFormat: options.outputFormat ?? outputFormat,
|
|
1661
|
-
signal: options.signal ?? signal,
|
|
1662
|
-
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1663
|
-
timeouts: options.timeouts ?? timeouts,
|
|
1664
|
-
sourceNodePath: options.sourceNodePath,
|
|
1665
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1666
|
-
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1667
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1668
|
-
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1669
|
-
resumeChunks: options.resumeChunks,
|
|
1670
|
-
resumeChunkIndices: options.resumeChunkIndices,
|
|
1671
|
-
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1672
|
-
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1673
|
-
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1674
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
1678
|
-
return synthesizeSsmlSafe(this, ssml, options);
|
|
1679
|
-
}
|
|
1680
|
-
async synthesizeChunksSafe(chunks, options = {}) {
|
|
1681
|
-
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
1682
|
-
...options,
|
|
1683
|
-
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
1684
|
-
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
1685
|
-
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
1686
|
-
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1687
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1688
|
-
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1689
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
1693
|
-
return this.synthesizeChunksSafe(chunks, options);
|
|
1694
|
-
}
|
|
1695
|
-
};
|
|
1696
|
-
_options = new WeakMap();
|
|
1697
|
-
|
|
1698
|
-
// src/voiceCatalog.ts
|
|
1699
|
-
var AZURE_VOICE_API_VERSION = "2025-10-01";
|
|
1700
|
-
function stringValue(value) {
|
|
1701
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1702
|
-
}
|
|
1703
|
-
function stringList(value) {
|
|
1704
|
-
if (!Array.isArray(value)) return [];
|
|
1705
|
-
return [...new Set(value.map(stringValue).filter((item) => item !== void 0))];
|
|
1706
|
-
}
|
|
1707
|
-
function normalizeStatus(value) {
|
|
1708
|
-
const status = stringValue(value)?.toLowerCase();
|
|
1709
|
-
if (status === "preview" || status === "deprecated" || status === "ga") return status;
|
|
1710
|
-
return void 0;
|
|
1711
|
-
}
|
|
1712
|
-
function normalizeRegions(region) {
|
|
1713
|
-
const regions = Array.isArray(region) ? region : [region];
|
|
1714
|
-
const result = [...new Set(regions.map((item) => item.trim()).filter(Boolean))];
|
|
1715
|
-
if (result.length === 0) throw new TypeError("At least one Azure Speech region is required.");
|
|
1716
|
-
return result;
|
|
1717
|
-
}
|
|
1718
|
-
async function fetchRegionVoices(region, apiKey) {
|
|
1719
|
-
const endpoint = `https://${encodeURIComponent(region)}.tts.speech.microsoft.com/cognitiveservices/voices/list`;
|
|
1720
|
-
const response = await fetch(endpoint, {
|
|
1721
|
-
headers: {
|
|
1722
|
-
Accept: "application/json",
|
|
1723
|
-
"Ocp-Apim-Subscription-Key": apiKey
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
if (!response.ok) {
|
|
1727
|
-
throw new Error(`Azure List Voices API request failed for region "${region}" with HTTP ${response.status}.`);
|
|
1728
|
-
}
|
|
1729
|
-
const payload = await response.json();
|
|
1730
|
-
if (!Array.isArray(payload)) throw new Error(`Azure List Voices API returned an invalid response for "${region}".`);
|
|
1731
|
-
return payload.filter((item) => Boolean(item && typeof item === "object"));
|
|
1732
|
-
}
|
|
1733
|
-
async function fetchAzureVoiceCatalog(options) {
|
|
1734
|
-
if (!options || typeof options.apiKey !== "string" || !options.apiKey.trim())
|
|
1735
|
-
throw new TypeError("An Azure Speech API key is required.");
|
|
1736
|
-
const regions = normalizeRegions(options.region);
|
|
1737
|
-
const payloads = await Promise.all(regions.map((region) => fetchRegionVoices(region, options.apiKey)));
|
|
1738
|
-
const voices = /* @__PURE__ */ new Map();
|
|
1739
|
-
for (let regionIndex = 0; regionIndex < payloads.length; regionIndex += 1) {
|
|
1740
|
-
const region = regions[regionIndex];
|
|
1741
|
-
for (const record of payloads[regionIndex]) {
|
|
1742
|
-
const name = stringValue(record.ShortName) ?? stringValue(record.Name);
|
|
1743
|
-
const locale = stringValue(record.Locale);
|
|
1744
|
-
if (!name || !locale) continue;
|
|
1745
|
-
const key = name.toLowerCase();
|
|
1746
|
-
const existing = voices.get(key);
|
|
1747
|
-
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
1748
|
-
const styles = stringList(record.StyleList);
|
|
1749
|
-
const status = normalizeStatus(record.Status);
|
|
1750
|
-
const supportedTags = stringList(record.SupportedTags);
|
|
1751
|
-
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
1752
|
-
const models = stringList(record.Models);
|
|
1753
|
-
const merged = {
|
|
1754
|
-
name: existing?.name ?? name,
|
|
1755
|
-
locale: existing?.locale ?? locale,
|
|
1756
|
-
regions: [.../* @__PURE__ */ new Set([...existing?.regions ?? [], region])]
|
|
1757
|
-
};
|
|
1758
|
-
const mergedSecondaryLocales = [.../* @__PURE__ */ new Set([...existing?.secondaryLocales ?? [], ...secondaryLocales])];
|
|
1759
|
-
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
1760
|
-
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
1761
|
-
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
1762
|
-
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
1763
|
-
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
1764
|
-
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
1765
|
-
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
1766
|
-
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
1767
|
-
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
1768
|
-
if (status) merged.status = status;
|
|
1769
|
-
else if (existing?.status) merged.status = existing.status;
|
|
1770
|
-
voices.set(key, merged);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
const sortedVoices = [...voices.values()].sort((first, second) => first.name.localeCompare(second.name));
|
|
1774
|
-
return {
|
|
1775
|
-
voices: sortedVoices,
|
|
1776
|
-
metadata: {
|
|
1777
|
-
voiceCount: sortedVoices.length,
|
|
1778
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1779
|
-
apiVersion: AZURE_VOICE_API_VERSION,
|
|
1780
|
-
regions,
|
|
1781
|
-
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
1782
|
-
regionDiffs: {}
|
|
1783
|
-
}
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
export {
|
|
1787
|
-
AudioFormatMismatchError,
|
|
1788
|
-
AzureTtsClient,
|
|
1789
|
-
AzureTtsError,
|
|
1790
|
-
AzureTtsSdkError,
|
|
1791
|
-
BatchChunkValidationError,
|
|
1792
|
-
ChunkValidationError,
|
|
1793
|
-
DEFAULT_OUTPUT_FORMAT,
|
|
1794
|
-
MergeError,
|
|
1795
|
-
SynthesisCancelledError,
|
|
1796
|
-
SynthesisTimeoutError,
|
|
1797
|
-
UnsupportedMergeFormatError,
|
|
1798
|
-
canMergeAudioFormat,
|
|
1799
|
-
computeChunkFingerprint,
|
|
1800
|
-
fetchAzureVoiceCatalog,
|
|
1801
|
-
getRetryAfterDelayMs,
|
|
1802
|
-
inspectAudioSpecification,
|
|
1803
|
-
mergeAudioBuffers,
|
|
1804
|
-
mergeSynthesisResults,
|
|
1805
|
-
resolveMergeAudioFormat,
|
|
1806
|
-
resolveMimeType,
|
|
1807
|
-
synthesizeSpeech,
|
|
1808
|
-
synthesizeSsml,
|
|
1809
|
-
synthesizeSsmlChunks,
|
|
1810
|
-
synthesizeSsmlChunksSafe,
|
|
1811
|
-
synthesizeSsmlSafe
|
|
1812
|
-
};
|
|
1813
|
-
//# sourceMappingURL=index.mjs.map
|