@ssml-builder-js/azure-tts-client 2.17.0 → 2.19.0
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 +20 -0
- package/dist/index.d.mts +123 -43
- package/dist/index.d.ts +123 -43
- package/dist/index.js +674 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +670 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +64 -8
- package/src/deadline.ts +52 -0
- package/src/errors.ts +86 -4
- package/src/index.ts +15 -1
- package/src/outputFormats.ts +3 -0
- package/src/safe.ts +115 -25
- package/src/synthesis.ts +508 -50
- package/src/types.ts +32 -1
- package/test/v218-pipeline.test.ts +91 -0
- package/test/v219-pipeline.test.ts +131 -0
package/dist/index.js
CHANGED
|
@@ -44,11 +44,14 @@ __export(index_exports, {
|
|
|
44
44
|
BatchChunkValidationError: () => BatchChunkValidationError,
|
|
45
45
|
ChunkValidationError: () => ChunkValidationError,
|
|
46
46
|
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
47
|
+
DeadlineController: () => DeadlineController,
|
|
48
|
+
IncompleteChunkSetError: () => IncompleteChunkSetError,
|
|
47
49
|
MergeError: () => MergeError,
|
|
48
50
|
SynthesisCancelledError: () => SynthesisCancelledError,
|
|
49
51
|
SynthesisTimeoutError: () => SynthesisTimeoutError,
|
|
50
52
|
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
51
53
|
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
54
|
+
computeChunkFingerprint: () => computeChunkFingerprint,
|
|
52
55
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
53
56
|
getRetryAfterDelayMs: () => getRetryAfterDelayMs,
|
|
54
57
|
inspectAudioSpecification: () => inspectAudioSpecification,
|
|
@@ -56,6 +59,7 @@ __export(index_exports, {
|
|
|
56
59
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
57
60
|
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
58
61
|
resolveMimeType: () => resolveMimeType,
|
|
62
|
+
serializeChunkError: () => serializeChunkError,
|
|
59
63
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
60
64
|
synthesizeSsml: () => synthesizeSsml,
|
|
61
65
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
@@ -127,6 +131,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
127
131
|
this.name = "SynthesisTimeoutError";
|
|
128
132
|
}
|
|
129
133
|
};
|
|
134
|
+
var IncompleteChunkSetError = class extends Error {
|
|
135
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
136
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
137
|
+
this.kind = "incomplete-chunk-set";
|
|
138
|
+
this.name = "IncompleteChunkSetError";
|
|
139
|
+
this.totalChunks = totalChunks;
|
|
140
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
141
|
+
}
|
|
142
|
+
};
|
|
130
143
|
var MergeError = class extends Error {
|
|
131
144
|
constructor(message, cause) {
|
|
132
145
|
super(message);
|
|
@@ -151,8 +164,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
151
164
|
this.format = format;
|
|
152
165
|
}
|
|
153
166
|
};
|
|
167
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
170
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
171
|
+
const code = kind === "validation-error" ? "VALIDATION_ERROR" : kind === "timeout" || /tim(?:e|ed) ?out|deadline/i.test(message) ? "TIMEOUT" : kind === "cancelled" || /cancel|abort/i.test(message) ? "CANCELLED" : kind === "audio-format-mismatch" || kind === "unsupported-format-error" ? "FORMAT_MISMATCH" : kind === "merge-error" || phase === "merge" ? "MERGE_ERROR" : "AZURE_API_ERROR";
|
|
172
|
+
const details = {};
|
|
173
|
+
if (error instanceof AzureTtsError) {
|
|
174
|
+
details.statusText = error.statusText;
|
|
175
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
176
|
+
}
|
|
177
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
178
|
+
details.totalChunks = error.totalChunks;
|
|
179
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
code,
|
|
183
|
+
phase,
|
|
184
|
+
message,
|
|
185
|
+
isOriginalFailure,
|
|
186
|
+
isRetryable: code === "AZURE_API_ERROR" && (status === 429 || status !== void 0 && status >= 500 || /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message)),
|
|
187
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
188
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
154
191
|
function toSynthesisError(error) {
|
|
155
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
192
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
156
193
|
return error;
|
|
157
194
|
const message = error instanceof Error ? error.message : String(error);
|
|
158
195
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -164,6 +201,55 @@ function createSpeechSdkError(error) {
|
|
|
164
201
|
return new AzureTtsSdkError(message);
|
|
165
202
|
}
|
|
166
203
|
|
|
204
|
+
// src/deadline.ts
|
|
205
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
206
|
+
var DeadlineController = class {
|
|
207
|
+
constructor(totalJobMs, parent) {
|
|
208
|
+
__privateAdd(this, _controller, new AbortController());
|
|
209
|
+
__privateAdd(this, _parent);
|
|
210
|
+
__privateAdd(this, _onParentAbort);
|
|
211
|
+
__privateAdd(this, _timer);
|
|
212
|
+
__privateAdd(this, _timedOut, false);
|
|
213
|
+
__privateSet(this, _parent, parent);
|
|
214
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
215
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
216
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
217
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
218
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
219
|
+
if (this.deadlineAtMs !== void 0) {
|
|
220
|
+
__privateSet(this, _timer, setTimeout(
|
|
221
|
+
() => {
|
|
222
|
+
__privateSet(this, _timedOut, true);
|
|
223
|
+
__privateGet(this, _controller).abort();
|
|
224
|
+
},
|
|
225
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
226
|
+
));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
get timedOut() {
|
|
230
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
231
|
+
}
|
|
232
|
+
get remainingMs() {
|
|
233
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
234
|
+
}
|
|
235
|
+
throwIfExpired() {
|
|
236
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
237
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
238
|
+
}
|
|
239
|
+
abort() {
|
|
240
|
+
__privateGet(this, _controller).abort();
|
|
241
|
+
}
|
|
242
|
+
dispose() {
|
|
243
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
244
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
_controller = new WeakMap();
|
|
248
|
+
_parent = new WeakMap();
|
|
249
|
+
_onParentAbort = new WeakMap();
|
|
250
|
+
_timer = new WeakMap();
|
|
251
|
+
_timedOut = new WeakMap();
|
|
252
|
+
|
|
167
253
|
// src/synthesis.ts
|
|
168
254
|
var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
169
255
|
var import_ssml_core = require("@ssml-builder-js/ssml-core");
|
|
@@ -215,6 +301,9 @@ var OUTPUT_FORMATS = {
|
|
|
215
301
|
function resolveMimeType(outputFormat) {
|
|
216
302
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
217
303
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
304
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
305
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
306
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
218
307
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
219
308
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
220
309
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -243,6 +332,32 @@ function createSpeechConfig(config) {
|
|
|
243
332
|
}
|
|
244
333
|
|
|
245
334
|
// src/synthesis.ts
|
|
335
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
336
|
+
const readAttribute = (name) => {
|
|
337
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
338
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
339
|
+
};
|
|
340
|
+
const headers = Object.fromEntries(
|
|
341
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
342
|
+
);
|
|
343
|
+
const payload = JSON.stringify({
|
|
344
|
+
ssml,
|
|
345
|
+
outputFormat,
|
|
346
|
+
region: options.region ?? "",
|
|
347
|
+
endpoint: options.endpoint ?? "",
|
|
348
|
+
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
349
|
+
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
350
|
+
customHeaders: headers,
|
|
351
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
352
|
+
});
|
|
353
|
+
let hash = 0xcbf29ce484222325n;
|
|
354
|
+
const mask = 0xffffffffffffffffn;
|
|
355
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
356
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
357
|
+
hash = hash * 0x100000001b3n & mask;
|
|
358
|
+
}
|
|
359
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
360
|
+
}
|
|
246
361
|
function ascii(bytes, offset, value) {
|
|
247
362
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
248
363
|
}
|
|
@@ -282,9 +397,11 @@ function parseWav(buffer) {
|
|
|
282
397
|
}
|
|
283
398
|
return { chunks, data, format };
|
|
284
399
|
}
|
|
285
|
-
function
|
|
286
|
-
const match =
|
|
287
|
-
|
|
400
|
+
function formatSampleRate(format) {
|
|
401
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
402
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
403
|
+
const value = Number(match[1]);
|
|
404
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
288
405
|
}
|
|
289
406
|
function formatChannels(format, fallback) {
|
|
290
407
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -292,11 +409,11 @@ function formatChannels(format, fallback) {
|
|
|
292
409
|
return fallback;
|
|
293
410
|
}
|
|
294
411
|
function formatAudioSpecification(format) {
|
|
295
|
-
const sampleRate =
|
|
412
|
+
const sampleRate = formatSampleRate(format);
|
|
296
413
|
const channels = formatChannels(format, 0);
|
|
297
414
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
298
415
|
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
299
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /
|
|
416
|
+
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";
|
|
300
417
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
301
418
|
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;
|
|
302
419
|
return {
|
|
@@ -309,7 +426,7 @@ function formatAudioSpecification(format) {
|
|
|
309
426
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
310
427
|
...container ? { container } : {},
|
|
311
428
|
isVbr: /vbr/i.test(format),
|
|
312
|
-
isCompressed: codec
|
|
429
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
313
430
|
};
|
|
314
431
|
}
|
|
315
432
|
function parseMp3Specification(buffer, format) {
|
|
@@ -351,6 +468,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
351
468
|
}
|
|
352
469
|
return void 0;
|
|
353
470
|
}
|
|
471
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
472
|
+
const first = bytes[offset];
|
|
473
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
474
|
+
let mask = 128;
|
|
475
|
+
let length = 1;
|
|
476
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
477
|
+
mask >>= 1;
|
|
478
|
+
length += 1;
|
|
479
|
+
}
|
|
480
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
481
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
482
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
483
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
484
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
485
|
+
return { value, length };
|
|
486
|
+
}
|
|
487
|
+
function readEbmlElement(bytes, offset) {
|
|
488
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
489
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
490
|
+
const dataStart = offset + id.length + size.length;
|
|
491
|
+
const dataEnd = dataStart + size.value;
|
|
492
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
493
|
+
return { id: id.value, dataStart, dataEnd };
|
|
494
|
+
}
|
|
495
|
+
function ebmlText(bytes, element) {
|
|
496
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
497
|
+
}
|
|
498
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
499
|
+
let offset = start;
|
|
500
|
+
while (offset < end) {
|
|
501
|
+
const element = readEbmlElement(bytes, offset);
|
|
502
|
+
if (element.id === id) return element;
|
|
503
|
+
offset = element.dataEnd;
|
|
504
|
+
}
|
|
505
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
506
|
+
return void 0;
|
|
507
|
+
}
|
|
508
|
+
function parseOggSpecification(buffer, format) {
|
|
509
|
+
const bytes = new Uint8Array(buffer);
|
|
510
|
+
let offset = 0;
|
|
511
|
+
let firstPayload;
|
|
512
|
+
let pages = 0;
|
|
513
|
+
while (offset < bytes.byteLength) {
|
|
514
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
515
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
516
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
517
|
+
const lacingStart = offset + 27;
|
|
518
|
+
const payloadStart = lacingStart + segmentCount;
|
|
519
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
520
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
521
|
+
const pageEnd = payloadStart + payloadLength;
|
|
522
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
523
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
524
|
+
offset = pageEnd;
|
|
525
|
+
pages += 1;
|
|
526
|
+
}
|
|
527
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
528
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
529
|
+
const version = firstPayload[8];
|
|
530
|
+
const channels = firstPayload[9] ?? 0;
|
|
531
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
532
|
+
12,
|
|
533
|
+
true
|
|
534
|
+
);
|
|
535
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
536
|
+
return {
|
|
537
|
+
format,
|
|
538
|
+
mimeType: "audio/ogg",
|
|
539
|
+
codec: "opus",
|
|
540
|
+
sampleRate,
|
|
541
|
+
channels,
|
|
542
|
+
container: "ogg",
|
|
543
|
+
isVbr: true,
|
|
544
|
+
isCompressed: true
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function parseWebmSpecification(buffer, format) {
|
|
548
|
+
const bytes = new Uint8Array(buffer);
|
|
549
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
550
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
551
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
552
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
553
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
554
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
555
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
556
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
557
|
+
let offset = tracks.dataStart;
|
|
558
|
+
let opusTrack;
|
|
559
|
+
while (offset < tracks.dataEnd) {
|
|
560
|
+
const track = readEbmlElement(bytes, offset);
|
|
561
|
+
if (track.id === 174) {
|
|
562
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
563
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
564
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
565
|
+
opusTrack = track;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
offset = track.dataEnd;
|
|
570
|
+
}
|
|
571
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
572
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
573
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
574
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
575
|
+
const sampleRate = sampling ? new DataView(
|
|
576
|
+
bytes.buffer,
|
|
577
|
+
bytes.byteOffset + sampling.dataStart,
|
|
578
|
+
sampling.dataEnd - sampling.dataStart
|
|
579
|
+
).getFloat64(0, false) : 0;
|
|
580
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
581
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
582
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
583
|
+
return {
|
|
584
|
+
format,
|
|
585
|
+
mimeType: "audio/webm",
|
|
586
|
+
codec: "opus",
|
|
587
|
+
sampleRate: Math.round(sampleRate),
|
|
588
|
+
channels: channelCount,
|
|
589
|
+
container: "webm",
|
|
590
|
+
isVbr: true,
|
|
591
|
+
isCompressed: true
|
|
592
|
+
};
|
|
593
|
+
}
|
|
354
594
|
function inspectAudioSpecification(buffer, format) {
|
|
355
595
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
356
596
|
const parsed = parseWav(buffer);
|
|
@@ -360,27 +600,78 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
360
600
|
const channels = view.getUint16(2, true);
|
|
361
601
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
362
602
|
const formatCode = view.getUint16(0, true);
|
|
603
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
604
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
363
605
|
return {
|
|
364
606
|
format,
|
|
365
607
|
mimeType: "audio/wav",
|
|
366
|
-
codec
|
|
608
|
+
codec,
|
|
367
609
|
sampleRate,
|
|
368
610
|
channels,
|
|
369
611
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
370
612
|
bitDepth: bitsPerSample,
|
|
371
613
|
container: "riff-wave",
|
|
372
614
|
isVbr: false,
|
|
373
|
-
isCompressed:
|
|
615
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
374
616
|
};
|
|
375
617
|
}
|
|
376
|
-
if (isMp3Format(format))
|
|
377
|
-
|
|
618
|
+
if (isMp3Format(format)) {
|
|
619
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
620
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
621
|
+
}
|
|
622
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
623
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
624
|
+
validateContainerFormat(specification2, format);
|
|
625
|
+
return specification2;
|
|
626
|
+
}
|
|
627
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
628
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
629
|
+
validateContainerFormat(specification2, format);
|
|
630
|
+
return specification2;
|
|
631
|
+
}
|
|
632
|
+
const specification = formatAudioSpecification(format);
|
|
633
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
634
|
+
return specification;
|
|
635
|
+
}
|
|
636
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
637
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
638
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
639
|
+
}
|
|
640
|
+
if (specification.codec === "siren") return;
|
|
641
|
+
if (specification.codec === "silk") {
|
|
642
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
643
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
647
|
+
if (specification.codec === "opus") {
|
|
648
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
649
|
+
const frameCountCode = packetCode & 3;
|
|
650
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
651
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
655
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function validateContainerFormat(specification, format) {
|
|
662
|
+
const expected = formatAudioSpecification(format);
|
|
663
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
664
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
665
|
+
expected,
|
|
666
|
+
specification
|
|
667
|
+
]);
|
|
668
|
+
}
|
|
378
669
|
}
|
|
379
670
|
function validateAudioSpecifications(specs) {
|
|
380
671
|
const first = specs[0];
|
|
381
672
|
if (!first) return;
|
|
382
673
|
const mismatch = specs.find(
|
|
383
|
-
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || 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
|
|
674
|
+
(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
|
|
384
675
|
);
|
|
385
676
|
if (mismatch)
|
|
386
677
|
throw new AudioFormatMismatchError(
|
|
@@ -457,12 +748,67 @@ function stripMp3Tags(buffer) {
|
|
|
457
748
|
function isMp3Format(format) {
|
|
458
749
|
return /(?:mp3|mpeg)/i.test(format);
|
|
459
750
|
}
|
|
751
|
+
function isOggFormat(format) {
|
|
752
|
+
return /ogg/i.test(format);
|
|
753
|
+
}
|
|
754
|
+
function isWebmFormat(format) {
|
|
755
|
+
return /webm/i.test(format);
|
|
756
|
+
}
|
|
460
757
|
function isWavFormat(format) {
|
|
461
758
|
return /(?:wav|wave|riff)/i.test(format);
|
|
462
759
|
}
|
|
463
760
|
function isRawFormat(format) {
|
|
464
761
|
return /^raw(?:-|$)/i.test(format);
|
|
465
762
|
}
|
|
763
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
764
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
765
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
766
|
+
}
|
|
767
|
+
let specification;
|
|
768
|
+
try {
|
|
769
|
+
specification = inspectAudioSpecification(merged, format);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (!allowExternalContainer) throw error;
|
|
772
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
773
|
+
}
|
|
774
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
775
|
+
const firstInput = inputSpecs[0];
|
|
776
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
777
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
778
|
+
...inputSpecs,
|
|
779
|
+
specification
|
|
780
|
+
]);
|
|
781
|
+
}
|
|
782
|
+
if (isRawFormat(format)) {
|
|
783
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
784
|
+
if (merged.byteLength !== expectedSize) {
|
|
785
|
+
throw new MergeError(
|
|
786
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
validateRawAudioBuffer(merged, specification);
|
|
790
|
+
}
|
|
791
|
+
return specification;
|
|
792
|
+
}
|
|
793
|
+
async function withinDeadline(value, deadline) {
|
|
794
|
+
if (!deadline) return value;
|
|
795
|
+
deadline.throwIfExpired();
|
|
796
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
797
|
+
let timer;
|
|
798
|
+
try {
|
|
799
|
+
return await Promise.race([
|
|
800
|
+
Promise.resolve(value),
|
|
801
|
+
new Promise((_resolve, reject) => {
|
|
802
|
+
timer = setTimeout(
|
|
803
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
804
|
+
deadline.remainingMs
|
|
805
|
+
);
|
|
806
|
+
})
|
|
807
|
+
]);
|
|
808
|
+
} finally {
|
|
809
|
+
if (timer) clearTimeout(timer);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
466
812
|
function resolveMergeAudioFormat(format) {
|
|
467
813
|
if (isWavFormat(format)) return "wav";
|
|
468
814
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -475,6 +821,7 @@ function canMergeAudioFormat(format) {
|
|
|
475
821
|
function mergeAudioBuffers(buffers, options) {
|
|
476
822
|
const format = typeof options === "string" ? options : options?.format;
|
|
477
823
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
824
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
478
825
|
try {
|
|
479
826
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
480
827
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -515,7 +862,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
515
862
|
}
|
|
516
863
|
}
|
|
517
864
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
518
|
-
async function
|
|
865
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
519
866
|
if (config.signal?.aborted) {
|
|
520
867
|
throw new SynthesisCancelledError();
|
|
521
868
|
}
|
|
@@ -645,6 +992,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
645
992
|
rejectWithError(err);
|
|
646
993
|
return;
|
|
647
994
|
}
|
|
995
|
+
let audioSpec;
|
|
996
|
+
try {
|
|
997
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
rejectWithError(error);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
648
1002
|
settled = true;
|
|
649
1003
|
cleanup();
|
|
650
1004
|
closeResources();
|
|
@@ -680,8 +1034,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
680
1034
|
resolve({
|
|
681
1035
|
audioData: result.audioData,
|
|
682
1036
|
durationMs,
|
|
683
|
-
audioSpec
|
|
684
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
1037
|
+
audioSpec,
|
|
1038
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
685
1039
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
686
1040
|
...requestId ? { requestId } : {},
|
|
687
1041
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -694,7 +1048,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
694
1048
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
695
1049
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
696
1050
|
}
|
|
697
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
1051
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
698
1052
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
699
1053
|
timeout = setTimeout(
|
|
700
1054
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -747,7 +1101,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
747
1101
|
}
|
|
748
1102
|
});
|
|
749
1103
|
}
|
|
750
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
1104
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
751
1105
|
const options = retryOptions ? {
|
|
752
1106
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
753
1107
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -758,17 +1112,42 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
758
1112
|
while (true) {
|
|
759
1113
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
760
1114
|
try {
|
|
761
|
-
return await
|
|
1115
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
762
1116
|
} catch (error) {
|
|
763
1117
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
764
1118
|
throw error;
|
|
765
1119
|
attempt += 1;
|
|
766
1120
|
const delayMs = retryDelay(options, attempt, error);
|
|
1121
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1122
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
1123
|
+
throw new SynthesisTimeoutError(
|
|
1124
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
767
1127
|
onRetry(attempt, delayMs);
|
|
768
|
-
await waitForRetry(delayMs, config.signal);
|
|
1128
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
769
1129
|
}
|
|
770
1130
|
}
|
|
771
1131
|
}
|
|
1132
|
+
async function synthesizeSsml(ssml, config) {
|
|
1133
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1134
|
+
try {
|
|
1135
|
+
deadline.throwIfExpired();
|
|
1136
|
+
const synthesisConfig = {
|
|
1137
|
+
...config,
|
|
1138
|
+
signal: deadline.signal,
|
|
1139
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1140
|
+
};
|
|
1141
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1142
|
+
deadline.throwIfExpired();
|
|
1143
|
+
return result;
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1146
|
+
throw error;
|
|
1147
|
+
} finally {
|
|
1148
|
+
deadline.dispose();
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
772
1151
|
function createAbortScope(parent, timeoutMs) {
|
|
773
1152
|
const controller = new AbortController();
|
|
774
1153
|
let didTimeout = false;
|
|
@@ -789,10 +1168,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
789
1168
|
abort: () => controller.abort()
|
|
790
1169
|
};
|
|
791
1170
|
}
|
|
792
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
1171
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
793
1172
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
794
1173
|
try {
|
|
795
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
1174
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
796
1175
|
} catch (error) {
|
|
797
1176
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
798
1177
|
throw error;
|
|
@@ -801,18 +1180,42 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
801
1180
|
}
|
|
802
1181
|
}
|
|
803
1182
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
804
|
-
const results = new Array(chunks.length);
|
|
805
1183
|
const totalChunks = chunks.length;
|
|
1184
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1185
|
+
const fingerprints = inputs.map(
|
|
1186
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1187
|
+
region: config.region,
|
|
1188
|
+
endpoint: config.endpoint,
|
|
1189
|
+
customHeaders: config.customHeaders,
|
|
1190
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1191
|
+
})
|
|
1192
|
+
);
|
|
1193
|
+
const results = new Array(totalChunks);
|
|
806
1194
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1195
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1196
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1197
|
+
chunkIndex,
|
|
1198
|
+
status: "pending",
|
|
1199
|
+
canResume: true
|
|
1200
|
+
}));
|
|
807
1201
|
for (const [index, cached] of cachedChunks) {
|
|
808
|
-
if (index
|
|
1202
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
1203
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
1204
|
+
if (isValid) {
|
|
1205
|
+
results[index] = { ...cached };
|
|
1206
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1207
|
+
} else {
|
|
1208
|
+
invalidCachedIndices.add(index);
|
|
1209
|
+
}
|
|
809
1210
|
}
|
|
810
1211
|
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
811
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1212
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1213
|
+
const jobStartedAt = Date.now();
|
|
1214
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1215
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
812
1216
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
813
1217
|
const report = (event) => config.onProgress?.(event);
|
|
814
|
-
for (const [index,
|
|
815
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1218
|
+
for (const [index, input] of inputs.entries()) {
|
|
816
1219
|
report({
|
|
817
1220
|
currentChunk: index,
|
|
818
1221
|
totalChunks,
|
|
@@ -834,8 +1237,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
834
1237
|
if (index >= chunks.length) return;
|
|
835
1238
|
if (!shouldSynthesize(index)) continue;
|
|
836
1239
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
837
|
-
const
|
|
838
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1240
|
+
const input = inputs[index];
|
|
839
1241
|
report({
|
|
840
1242
|
currentChunk: completed,
|
|
841
1243
|
totalChunks,
|
|
@@ -851,7 +1253,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
851
1253
|
input.ssml,
|
|
852
1254
|
{
|
|
853
1255
|
...config,
|
|
854
|
-
signal:
|
|
1256
|
+
signal: deadline.signal,
|
|
855
1257
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
856
1258
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
857
1259
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -872,9 +1274,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
872
1274
|
retryAttempt,
|
|
873
1275
|
nextRetryDelayMs,
|
|
874
1276
|
isRetrying: true
|
|
875
|
-
})
|
|
1277
|
+
}),
|
|
1278
|
+
jobDeadlineAt
|
|
876
1279
|
);
|
|
877
1280
|
results[index] = result;
|
|
1281
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
878
1282
|
completed += 1;
|
|
879
1283
|
report({
|
|
880
1284
|
currentChunk: completed,
|
|
@@ -886,7 +1290,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
886
1290
|
durationMs: Date.now() - startedAt
|
|
887
1291
|
});
|
|
888
1292
|
} catch (error) {
|
|
889
|
-
|
|
1293
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
1294
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
1295
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1296
|
+
chunkStates[index] = {
|
|
1297
|
+
chunkIndex: index,
|
|
1298
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1299
|
+
isOriginalFailure: !wasCancelled,
|
|
1300
|
+
canResume: true,
|
|
1301
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1302
|
+
};
|
|
890
1303
|
report({
|
|
891
1304
|
currentChunk: completed,
|
|
892
1305
|
totalChunks,
|
|
@@ -897,7 +1310,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
897
1310
|
durationMs: Date.now() - startedAt,
|
|
898
1311
|
error
|
|
899
1312
|
});
|
|
900
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
901
1313
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
902
1314
|
return;
|
|
903
1315
|
}
|
|
@@ -906,26 +1318,47 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
906
1318
|
try {
|
|
907
1319
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
908
1320
|
if (firstError) throw firstError;
|
|
1321
|
+
const missingChunkIndices = Array.from(
|
|
1322
|
+
{ length: totalChunks },
|
|
1323
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1324
|
+
).filter((index) => index !== void 0);
|
|
1325
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
909
1326
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
910
1327
|
return await mergeSynthesisResults(orderedResults, {
|
|
911
1328
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
912
|
-
signal:
|
|
1329
|
+
signal: deadline.signal,
|
|
913
1330
|
customMerger: config.customMerger,
|
|
914
1331
|
outputMimeType: config.outputMimeType,
|
|
915
|
-
postMergeValidator: config.postMergeValidator
|
|
1332
|
+
postMergeValidator: config.postMergeValidator,
|
|
1333
|
+
deadline
|
|
916
1334
|
});
|
|
917
1335
|
} catch (error) {
|
|
1336
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
1337
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1338
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1339
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const synthesizedChunks = results.flatMap(
|
|
1344
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1345
|
+
);
|
|
918
1346
|
const partial = {
|
|
919
|
-
synthesizedChunks
|
|
920
|
-
completedChunks:
|
|
921
|
-
pendingChunkIndices:
|
|
1347
|
+
synthesizedChunks,
|
|
1348
|
+
completedChunks: synthesizedChunks,
|
|
1349
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1350
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1351
|
+
),
|
|
922
1352
|
failedChunkIndices: [...failedIndices],
|
|
1353
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1354
|
+
chunkStates,
|
|
923
1355
|
totalChunks
|
|
924
1356
|
};
|
|
925
1357
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
926
1358
|
throw error;
|
|
927
1359
|
} finally {
|
|
928
1360
|
scope.dispose();
|
|
1361
|
+
deadline.dispose();
|
|
929
1362
|
}
|
|
930
1363
|
}
|
|
931
1364
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -1003,30 +1436,44 @@ function mergeSynthesisResults(results, options) {
|
|
|
1003
1436
|
const format = resolvedOptions?.format;
|
|
1004
1437
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
1005
1438
|
const buffers = results.map((result) => result.audioData);
|
|
1006
|
-
const inputSpecs = results.map((result) =>
|
|
1439
|
+
const inputSpecs = results.map((result) => {
|
|
1440
|
+
if (result.audioSpec) return result.audioSpec;
|
|
1441
|
+
try {
|
|
1442
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1007
1448
|
validateAudioSpecifications(inputSpecs);
|
|
1008
|
-
const
|
|
1449
|
+
const deadline = resolvedOptions.deadline;
|
|
1450
|
+
deadline?.throwIfExpired();
|
|
1451
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
1009
1452
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1010
1453
|
if (resolvedOptions.customMerger) {
|
|
1011
|
-
return
|
|
1012
|
-
()
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1454
|
+
return withinDeadline(
|
|
1455
|
+
Promise.resolve().then(
|
|
1456
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1457
|
+
format,
|
|
1458
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1459
|
+
inputSpecs,
|
|
1460
|
+
signal
|
|
1461
|
+
})
|
|
1462
|
+
),
|
|
1463
|
+
deadline
|
|
1018
1464
|
).then((merged) => {
|
|
1465
|
+
deadline?.throwIfExpired();
|
|
1019
1466
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
1020
|
-
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
1021
|
-
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
1022
1467
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1023
|
-
const
|
|
1024
|
-
results,
|
|
1468
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
1025
1469
|
merged,
|
|
1026
1470
|
format,
|
|
1027
|
-
|
|
1028
|
-
|
|
1471
|
+
buffers,
|
|
1472
|
+
inputSpecs,
|
|
1473
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1474
|
+
true
|
|
1029
1475
|
);
|
|
1476
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
1030
1477
|
return Promise.resolve(
|
|
1031
1478
|
resolvedOptions.postMergeValidator?.(result, {
|
|
1032
1479
|
format,
|
|
@@ -1035,6 +1482,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1035
1482
|
signal
|
|
1036
1483
|
})
|
|
1037
1484
|
).then((valid) => {
|
|
1485
|
+
deadline?.throwIfExpired();
|
|
1038
1486
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1039
1487
|
return result;
|
|
1040
1488
|
});
|
|
@@ -1045,6 +1493,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1045
1493
|
});
|
|
1046
1494
|
}
|
|
1047
1495
|
try {
|
|
1496
|
+
deadline?.throwIfExpired();
|
|
1048
1497
|
const result = createMergedResult(
|
|
1049
1498
|
results,
|
|
1050
1499
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -1060,12 +1509,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
1060
1509
|
signal
|
|
1061
1510
|
});
|
|
1062
1511
|
if (validation instanceof Promise)
|
|
1063
|
-
return validation.then((valid) => {
|
|
1512
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
1513
|
+
deadline?.throwIfExpired();
|
|
1064
1514
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1065
1515
|
return result;
|
|
1066
1516
|
});
|
|
1067
1517
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1068
1518
|
}
|
|
1519
|
+
deadline?.throwIfExpired();
|
|
1069
1520
|
return result;
|
|
1070
1521
|
} catch (error) {
|
|
1071
1522
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -1156,7 +1607,7 @@ function resolveConcurrency2(value, total) {
|
|
|
1156
1607
|
if (value === Infinity) return Math.max(1, total);
|
|
1157
1608
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1158
1609
|
}
|
|
1159
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1610
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1160
1611
|
const retry = options ? {
|
|
1161
1612
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1162
1613
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -1173,6 +1624,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
1173
1624
|
throw error;
|
|
1174
1625
|
attempt += 1;
|
|
1175
1626
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1627
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1628
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1629
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1630
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1631
|
+
}
|
|
1176
1632
|
onRetry(attempt, delayMs);
|
|
1177
1633
|
if (delayMs > 0)
|
|
1178
1634
|
await new Promise((resolve, reject) => {
|
|
@@ -1206,14 +1662,19 @@ function sharedValidationOptions(options, signal) {
|
|
|
1206
1662
|
};
|
|
1207
1663
|
}
|
|
1208
1664
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1209
|
-
const
|
|
1665
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1666
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1210
1667
|
const diagnostics = await Promise.resolve((0, import_ssml_core2.validateAzureSsml)(ssml, validationOptions));
|
|
1211
|
-
if (
|
|
1212
|
-
const error = toSynthesisError(
|
|
1668
|
+
if (deadline.signal.aborted) {
|
|
1669
|
+
const error = toSynthesisError(
|
|
1670
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1671
|
+
);
|
|
1672
|
+
deadline.dispose();
|
|
1213
1673
|
return failure(error);
|
|
1214
1674
|
}
|
|
1215
1675
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1216
1676
|
if (errors.length > 0) {
|
|
1677
|
+
deadline.dispose();
|
|
1217
1678
|
return failure({
|
|
1218
1679
|
kind: "validation-error",
|
|
1219
1680
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
@@ -1226,23 +1687,30 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
1226
1687
|
success: true,
|
|
1227
1688
|
status: "success",
|
|
1228
1689
|
value: await client.synthesizeSsml(ssml, {
|
|
1229
|
-
signal:
|
|
1690
|
+
signal: deadline.signal,
|
|
1230
1691
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1231
|
-
timeouts: options.timeouts
|
|
1692
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1232
1693
|
})
|
|
1233
1694
|
};
|
|
1234
1695
|
} catch (error) {
|
|
1696
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1235
1697
|
const synthesisError = toSynthesisError(error);
|
|
1236
1698
|
return failure(synthesisError);
|
|
1699
|
+
} finally {
|
|
1700
|
+
deadline.dispose();
|
|
1237
1701
|
}
|
|
1238
1702
|
}
|
|
1239
1703
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1704
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1240
1705
|
const validationOptions = sharedValidationOptions(
|
|
1241
1706
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1242
|
-
|
|
1707
|
+
deadline.signal
|
|
1243
1708
|
);
|
|
1244
|
-
if (
|
|
1245
|
-
const error = toSynthesisError(
|
|
1709
|
+
if (deadline.signal.aborted) {
|
|
1710
|
+
const error = toSynthesisError(
|
|
1711
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1712
|
+
);
|
|
1713
|
+
deadline.dispose();
|
|
1246
1714
|
return failure(error);
|
|
1247
1715
|
}
|
|
1248
1716
|
const pending = (index, status, error) => {
|
|
@@ -1274,14 +1742,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1274
1742
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1275
1743
|
})
|
|
1276
1744
|
);
|
|
1277
|
-
if (
|
|
1745
|
+
if (deadline.signal.aborted) {
|
|
1278
1746
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1747
|
+
deadline.dispose();
|
|
1279
1748
|
return failure(error);
|
|
1280
1749
|
}
|
|
1281
1750
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1282
1751
|
if (chunkDiagnostics.length > 0) {
|
|
1283
1752
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1284
1753
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1754
|
+
deadline.dispose();
|
|
1285
1755
|
return failure(error);
|
|
1286
1756
|
}
|
|
1287
1757
|
let fallbackJobScope;
|
|
@@ -1294,9 +1764,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1294
1764
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1295
1765
|
onProgress: options.onProgress,
|
|
1296
1766
|
outputFormat: options.outputFormat,
|
|
1297
|
-
signal:
|
|
1767
|
+
signal: deadline.signal,
|
|
1298
1768
|
timeoutMs: options.timeoutMs,
|
|
1299
|
-
timeouts: options.timeouts,
|
|
1769
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1300
1770
|
sourceNodePath: options.sourceNodePath,
|
|
1301
1771
|
concurrency: options.concurrency,
|
|
1302
1772
|
retryOptions: options.retryOptions,
|
|
@@ -1305,18 +1775,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1305
1775
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1306
1776
|
customMerger: options.customMerger,
|
|
1307
1777
|
outputMimeType: options.outputMimeType,
|
|
1308
|
-
postMergeValidator: options.postMergeValidator
|
|
1778
|
+
postMergeValidator: options.postMergeValidator,
|
|
1779
|
+
resumeValidation: options.resumeValidation,
|
|
1780
|
+
customHeaders: options.customHeaders,
|
|
1781
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1309
1782
|
});
|
|
1310
1783
|
return { ok: true, success: true, status: "success", value };
|
|
1311
1784
|
}
|
|
1785
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1786
|
+
const fingerprints = inputs.map(
|
|
1787
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1788
|
+
customHeaders: options.customHeaders,
|
|
1789
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1312
1792
|
const results = new Array(chunks.length);
|
|
1793
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1794
|
+
chunkIndex,
|
|
1795
|
+
status: "pending",
|
|
1796
|
+
canResume: true
|
|
1797
|
+
}));
|
|
1313
1798
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1799
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1314
1800
|
for (const [index, cached] of cachedChunks) {
|
|
1315
|
-
if (index
|
|
1801
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
1802
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1803
|
+
results[index] = cached;
|
|
1804
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1805
|
+
} else invalidCachedIndices.add(index);
|
|
1316
1806
|
}
|
|
1317
1807
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1318
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1319
|
-
const
|
|
1808
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1809
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1810
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1320
1811
|
fallbackJobScope = jobScope;
|
|
1321
1812
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
1322
1813
|
let firstError;
|
|
@@ -1328,7 +1819,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1328
1819
|
const index = nextIndex++;
|
|
1329
1820
|
if (index >= chunks.length) return;
|
|
1330
1821
|
if (!shouldSynthesize(index)) continue;
|
|
1331
|
-
if (
|
|
1822
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1823
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1332
1826
|
const chunk = chunks[index];
|
|
1333
1827
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1334
1828
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -1337,8 +1831,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1337
1831
|
const startedAt = Date.now();
|
|
1338
1832
|
try {
|
|
1339
1833
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1340
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
1341
|
-
const chunkSignal = chunkScope?.signal ??
|
|
1834
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1835
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1342
1836
|
let result;
|
|
1343
1837
|
try {
|
|
1344
1838
|
result = await retryableSynthesis(
|
|
@@ -1361,10 +1855,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1361
1855
|
retryAttempt,
|
|
1362
1856
|
nextRetryDelayMs,
|
|
1363
1857
|
isRetrying: true
|
|
1364
|
-
})
|
|
1858
|
+
}),
|
|
1859
|
+
jobDeadlineAt
|
|
1365
1860
|
);
|
|
1366
1861
|
} catch (error) {
|
|
1367
|
-
if (chunkScope?.timedOut())
|
|
1862
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1368
1863
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1369
1864
|
throw error;
|
|
1370
1865
|
} finally {
|
|
@@ -1413,6 +1908,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1413
1908
|
}))
|
|
1414
1909
|
} : {}
|
|
1415
1910
|
};
|
|
1911
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1416
1912
|
completed += 1;
|
|
1417
1913
|
options.onProgress?.({
|
|
1418
1914
|
currentChunk: completed,
|
|
@@ -1424,7 +1920,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1424
1920
|
durationMs: Date.now() - startedAt
|
|
1425
1921
|
});
|
|
1426
1922
|
} catch (error) {
|
|
1427
|
-
|
|
1923
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1924
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1925
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1926
|
+
chunkStates[index] = {
|
|
1927
|
+
chunkIndex: index,
|
|
1928
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1929
|
+
isOriginalFailure: !wasCancelled,
|
|
1930
|
+
canResume: true,
|
|
1931
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1932
|
+
};
|
|
1428
1933
|
options.onProgress?.({
|
|
1429
1934
|
currentChunk: completed,
|
|
1430
1935
|
totalChunks: chunks.length,
|
|
@@ -1436,23 +1941,41 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1436
1941
|
error
|
|
1437
1942
|
});
|
|
1438
1943
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1439
|
-
firstError ?? (firstError = error);
|
|
1440
1944
|
return;
|
|
1441
1945
|
}
|
|
1442
1946
|
}
|
|
1443
1947
|
};
|
|
1444
1948
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1949
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1950
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1951
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1952
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1445
1956
|
if (failedIndices.size > 0) {
|
|
1446
1957
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1958
|
+
const synthesizedChunks = results.flatMap(
|
|
1959
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1960
|
+
);
|
|
1447
1961
|
error.partialResult = {
|
|
1448
|
-
synthesizedChunks
|
|
1449
|
-
completedChunks:
|
|
1450
|
-
pendingChunkIndices:
|
|
1962
|
+
synthesizedChunks,
|
|
1963
|
+
completedChunks: synthesizedChunks,
|
|
1964
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1965
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1966
|
+
),
|
|
1451
1967
|
failedChunkIndices: [...failedIndices],
|
|
1968
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1969
|
+
chunkStates,
|
|
1452
1970
|
totalChunks: chunks.length
|
|
1453
1971
|
};
|
|
1454
1972
|
throw error;
|
|
1455
1973
|
}
|
|
1974
|
+
const missingChunkIndices = Array.from(
|
|
1975
|
+
{ length: chunks.length },
|
|
1976
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1977
|
+
).filter((index) => index !== void 0);
|
|
1978
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1456
1979
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1457
1980
|
return {
|
|
1458
1981
|
ok: true,
|
|
@@ -1460,7 +1983,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1460
1983
|
status: "success",
|
|
1461
1984
|
value: await mergeSynthesisResults(orderedResults, {
|
|
1462
1985
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1463
|
-
signal: jobScope?.signal ??
|
|
1986
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
1464
1987
|
customMerger: options.customMerger,
|
|
1465
1988
|
outputMimeType: options.outputMimeType,
|
|
1466
1989
|
postMergeValidator: options.postMergeValidator
|
|
@@ -1471,6 +1994,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1471
1994
|
return failure(synthesisError, partialResultFrom(error));
|
|
1472
1995
|
} finally {
|
|
1473
1996
|
fallbackJobScope?.dispose();
|
|
1997
|
+
deadline.dispose();
|
|
1474
1998
|
}
|
|
1475
1999
|
}
|
|
1476
2000
|
function withValidationSignal(options, signal) {
|
|
@@ -1491,14 +2015,43 @@ var AzureTtsClient = class {
|
|
|
1491
2015
|
__privateSet(this, _options, options);
|
|
1492
2016
|
}
|
|
1493
2017
|
async synthesize(ssml) {
|
|
1494
|
-
const {
|
|
2018
|
+
const {
|
|
2019
|
+
region,
|
|
2020
|
+
subscriptionKey,
|
|
2021
|
+
outputFormat,
|
|
2022
|
+
signal,
|
|
2023
|
+
timeoutMs,
|
|
2024
|
+
timeouts,
|
|
2025
|
+
customHeaders,
|
|
2026
|
+
fingerprintSchemaVersion
|
|
2027
|
+
} = __privateGet(this, _options);
|
|
1495
2028
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1496
2029
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1497
|
-
const config = {
|
|
2030
|
+
const config = {
|
|
2031
|
+
endpoint,
|
|
2032
|
+
region,
|
|
2033
|
+
subscriptionKey,
|
|
2034
|
+
outputFormat,
|
|
2035
|
+
signal,
|
|
2036
|
+
timeoutMs,
|
|
2037
|
+
timeouts,
|
|
2038
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
2039
|
+
customHeaders,
|
|
2040
|
+
fingerprintSchemaVersion
|
|
2041
|
+
};
|
|
1498
2042
|
return synthesizeSpeech(ssml, config);
|
|
1499
2043
|
}
|
|
1500
2044
|
async synthesizeSsml(ssml, options = {}) {
|
|
1501
|
-
const {
|
|
2045
|
+
const {
|
|
2046
|
+
region,
|
|
2047
|
+
subscriptionKey,
|
|
2048
|
+
outputFormat,
|
|
2049
|
+
signal,
|
|
2050
|
+
timeoutMs,
|
|
2051
|
+
timeouts,
|
|
2052
|
+
customHeaders,
|
|
2053
|
+
fingerprintSchemaVersion
|
|
2054
|
+
} = __privateGet(this, _options);
|
|
1502
2055
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1503
2056
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1504
2057
|
return synthesizeSsml(ssml, {
|
|
@@ -1511,11 +2064,28 @@ var AzureTtsClient = class {
|
|
|
1511
2064
|
timeouts: options.timeouts ?? timeouts,
|
|
1512
2065
|
sourceNodePath: options.sourceNodePath,
|
|
1513
2066
|
sourceTextSegments: options.sourceTextSegments,
|
|
1514
|
-
sourceMarkers: options.sourceMarkers
|
|
2067
|
+
sourceMarkers: options.sourceMarkers,
|
|
2068
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2069
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2070
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2071
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2072
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2073
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2074
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2075
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1515
2076
|
});
|
|
1516
2077
|
}
|
|
1517
2078
|
async synthesizeChunks(chunks, options = {}) {
|
|
1518
|
-
const {
|
|
2079
|
+
const {
|
|
2080
|
+
region,
|
|
2081
|
+
subscriptionKey,
|
|
2082
|
+
outputFormat,
|
|
2083
|
+
signal,
|
|
2084
|
+
timeoutMs,
|
|
2085
|
+
timeouts,
|
|
2086
|
+
customHeaders,
|
|
2087
|
+
fingerprintSchemaVersion
|
|
2088
|
+
} = __privateGet(this, _options);
|
|
1519
2089
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1520
2090
|
return synthesizeSsmlChunks(chunks, {
|
|
1521
2091
|
endpoint,
|
|
@@ -1529,12 +2099,15 @@ var AzureTtsClient = class {
|
|
|
1529
2099
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1530
2100
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1531
2101
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1532
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
2102
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1533
2103
|
resumeChunks: options.resumeChunks,
|
|
1534
2104
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1535
|
-
customMerger: options.customMerger,
|
|
1536
|
-
outputMimeType: options.outputMimeType,
|
|
1537
|
-
postMergeValidator: options.postMergeValidator
|
|
2105
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2106
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2107
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2108
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2109
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2110
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1538
2111
|
});
|
|
1539
2112
|
}
|
|
1540
2113
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1549,7 +2122,14 @@ var AzureTtsClient = class {
|
|
|
1549
2122
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1550
2123
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1551
2124
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1552
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
2125
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2126
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2127
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2128
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2129
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2130
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2131
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2132
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
1553
2133
|
});
|
|
1554
2134
|
}
|
|
1555
2135
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1655,11 +2235,14 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1655
2235
|
BatchChunkValidationError,
|
|
1656
2236
|
ChunkValidationError,
|
|
1657
2237
|
DEFAULT_OUTPUT_FORMAT,
|
|
2238
|
+
DeadlineController,
|
|
2239
|
+
IncompleteChunkSetError,
|
|
1658
2240
|
MergeError,
|
|
1659
2241
|
SynthesisCancelledError,
|
|
1660
2242
|
SynthesisTimeoutError,
|
|
1661
2243
|
UnsupportedMergeFormatError,
|
|
1662
2244
|
canMergeAudioFormat,
|
|
2245
|
+
computeChunkFingerprint,
|
|
1663
2246
|
fetchAzureVoiceCatalog,
|
|
1664
2247
|
getRetryAfterDelayMs,
|
|
1665
2248
|
inspectAudioSpecification,
|
|
@@ -1667,6 +2250,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1667
2250
|
mergeSynthesisResults,
|
|
1668
2251
|
resolveMergeAudioFormat,
|
|
1669
2252
|
resolveMimeType,
|
|
2253
|
+
serializeChunkError,
|
|
1670
2254
|
synthesizeSpeech,
|
|
1671
2255
|
synthesizeSsml,
|
|
1672
2256
|
synthesizeSsmlChunks,
|