@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.mjs
CHANGED
|
@@ -69,6 +69,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
69
69
|
this.name = "SynthesisTimeoutError";
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
var IncompleteChunkSetError = class extends Error {
|
|
73
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
74
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
75
|
+
this.kind = "incomplete-chunk-set";
|
|
76
|
+
this.name = "IncompleteChunkSetError";
|
|
77
|
+
this.totalChunks = totalChunks;
|
|
78
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
79
|
+
}
|
|
80
|
+
};
|
|
72
81
|
var MergeError = class extends Error {
|
|
73
82
|
constructor(message, cause) {
|
|
74
83
|
super(message);
|
|
@@ -93,8 +102,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
93
102
|
this.format = format;
|
|
94
103
|
}
|
|
95
104
|
};
|
|
105
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
108
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
109
|
+
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";
|
|
110
|
+
const details = {};
|
|
111
|
+
if (error instanceof AzureTtsError) {
|
|
112
|
+
details.statusText = error.statusText;
|
|
113
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
114
|
+
}
|
|
115
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
116
|
+
details.totalChunks = error.totalChunks;
|
|
117
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
code,
|
|
121
|
+
phase,
|
|
122
|
+
message,
|
|
123
|
+
isOriginalFailure,
|
|
124
|
+
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)),
|
|
125
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
126
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
96
129
|
function toSynthesisError(error) {
|
|
97
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
130
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
98
131
|
return error;
|
|
99
132
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
133
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -106,6 +139,55 @@ function createSpeechSdkError(error) {
|
|
|
106
139
|
return new AzureTtsSdkError(message);
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
// src/deadline.ts
|
|
143
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
144
|
+
var DeadlineController = class {
|
|
145
|
+
constructor(totalJobMs, parent) {
|
|
146
|
+
__privateAdd(this, _controller, new AbortController());
|
|
147
|
+
__privateAdd(this, _parent);
|
|
148
|
+
__privateAdd(this, _onParentAbort);
|
|
149
|
+
__privateAdd(this, _timer);
|
|
150
|
+
__privateAdd(this, _timedOut, false);
|
|
151
|
+
__privateSet(this, _parent, parent);
|
|
152
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
153
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
154
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
155
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
156
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
157
|
+
if (this.deadlineAtMs !== void 0) {
|
|
158
|
+
__privateSet(this, _timer, setTimeout(
|
|
159
|
+
() => {
|
|
160
|
+
__privateSet(this, _timedOut, true);
|
|
161
|
+
__privateGet(this, _controller).abort();
|
|
162
|
+
},
|
|
163
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
get timedOut() {
|
|
168
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
169
|
+
}
|
|
170
|
+
get remainingMs() {
|
|
171
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
172
|
+
}
|
|
173
|
+
throwIfExpired() {
|
|
174
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
175
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
176
|
+
}
|
|
177
|
+
abort() {
|
|
178
|
+
__privateGet(this, _controller).abort();
|
|
179
|
+
}
|
|
180
|
+
dispose() {
|
|
181
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
182
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
_controller = new WeakMap();
|
|
186
|
+
_parent = new WeakMap();
|
|
187
|
+
_onParentAbort = new WeakMap();
|
|
188
|
+
_timer = new WeakMap();
|
|
189
|
+
_timedOut = new WeakMap();
|
|
190
|
+
|
|
109
191
|
// src/synthesis.ts
|
|
110
192
|
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
111
193
|
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
@@ -157,6 +239,9 @@ var OUTPUT_FORMATS = {
|
|
|
157
239
|
function resolveMimeType(outputFormat) {
|
|
158
240
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
159
241
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
242
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
243
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
244
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
160
245
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
161
246
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
162
247
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -185,6 +270,32 @@ function createSpeechConfig(config) {
|
|
|
185
270
|
}
|
|
186
271
|
|
|
187
272
|
// src/synthesis.ts
|
|
273
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
274
|
+
const readAttribute = (name) => {
|
|
275
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
276
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
277
|
+
};
|
|
278
|
+
const headers = Object.fromEntries(
|
|
279
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
280
|
+
);
|
|
281
|
+
const payload = JSON.stringify({
|
|
282
|
+
ssml,
|
|
283
|
+
outputFormat,
|
|
284
|
+
region: options.region ?? "",
|
|
285
|
+
endpoint: options.endpoint ?? "",
|
|
286
|
+
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
287
|
+
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
288
|
+
customHeaders: headers,
|
|
289
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
290
|
+
});
|
|
291
|
+
let hash = 0xcbf29ce484222325n;
|
|
292
|
+
const mask = 0xffffffffffffffffn;
|
|
293
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
294
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
295
|
+
hash = hash * 0x100000001b3n & mask;
|
|
296
|
+
}
|
|
297
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
298
|
+
}
|
|
188
299
|
function ascii(bytes, offset, value) {
|
|
189
300
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
190
301
|
}
|
|
@@ -224,9 +335,11 @@ function parseWav(buffer) {
|
|
|
224
335
|
}
|
|
225
336
|
return { chunks, data, format };
|
|
226
337
|
}
|
|
227
|
-
function
|
|
228
|
-
const match =
|
|
229
|
-
|
|
338
|
+
function formatSampleRate(format) {
|
|
339
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
340
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
341
|
+
const value = Number(match[1]);
|
|
342
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
230
343
|
}
|
|
231
344
|
function formatChannels(format, fallback) {
|
|
232
345
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -234,11 +347,11 @@ function formatChannels(format, fallback) {
|
|
|
234
347
|
return fallback;
|
|
235
348
|
}
|
|
236
349
|
function formatAudioSpecification(format) {
|
|
237
|
-
const sampleRate =
|
|
350
|
+
const sampleRate = formatSampleRate(format);
|
|
238
351
|
const channels = formatChannels(format, 0);
|
|
239
352
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
240
353
|
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
241
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /
|
|
354
|
+
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";
|
|
242
355
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
243
356
|
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;
|
|
244
357
|
return {
|
|
@@ -251,7 +364,7 @@ function formatAudioSpecification(format) {
|
|
|
251
364
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
252
365
|
...container ? { container } : {},
|
|
253
366
|
isVbr: /vbr/i.test(format),
|
|
254
|
-
isCompressed: codec
|
|
367
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
255
368
|
};
|
|
256
369
|
}
|
|
257
370
|
function parseMp3Specification(buffer, format) {
|
|
@@ -293,6 +406,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
293
406
|
}
|
|
294
407
|
return void 0;
|
|
295
408
|
}
|
|
409
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
410
|
+
const first = bytes[offset];
|
|
411
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
412
|
+
let mask = 128;
|
|
413
|
+
let length = 1;
|
|
414
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
415
|
+
mask >>= 1;
|
|
416
|
+
length += 1;
|
|
417
|
+
}
|
|
418
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
419
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
420
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
421
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
422
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
423
|
+
return { value, length };
|
|
424
|
+
}
|
|
425
|
+
function readEbmlElement(bytes, offset) {
|
|
426
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
427
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
428
|
+
const dataStart = offset + id.length + size.length;
|
|
429
|
+
const dataEnd = dataStart + size.value;
|
|
430
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
431
|
+
return { id: id.value, dataStart, dataEnd };
|
|
432
|
+
}
|
|
433
|
+
function ebmlText(bytes, element) {
|
|
434
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
435
|
+
}
|
|
436
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
437
|
+
let offset = start;
|
|
438
|
+
while (offset < end) {
|
|
439
|
+
const element = readEbmlElement(bytes, offset);
|
|
440
|
+
if (element.id === id) return element;
|
|
441
|
+
offset = element.dataEnd;
|
|
442
|
+
}
|
|
443
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
function parseOggSpecification(buffer, format) {
|
|
447
|
+
const bytes = new Uint8Array(buffer);
|
|
448
|
+
let offset = 0;
|
|
449
|
+
let firstPayload;
|
|
450
|
+
let pages = 0;
|
|
451
|
+
while (offset < bytes.byteLength) {
|
|
452
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
453
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
454
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
455
|
+
const lacingStart = offset + 27;
|
|
456
|
+
const payloadStart = lacingStart + segmentCount;
|
|
457
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
458
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
459
|
+
const pageEnd = payloadStart + payloadLength;
|
|
460
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
461
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
462
|
+
offset = pageEnd;
|
|
463
|
+
pages += 1;
|
|
464
|
+
}
|
|
465
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
466
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
467
|
+
const version = firstPayload[8];
|
|
468
|
+
const channels = firstPayload[9] ?? 0;
|
|
469
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
470
|
+
12,
|
|
471
|
+
true
|
|
472
|
+
);
|
|
473
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
474
|
+
return {
|
|
475
|
+
format,
|
|
476
|
+
mimeType: "audio/ogg",
|
|
477
|
+
codec: "opus",
|
|
478
|
+
sampleRate,
|
|
479
|
+
channels,
|
|
480
|
+
container: "ogg",
|
|
481
|
+
isVbr: true,
|
|
482
|
+
isCompressed: true
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseWebmSpecification(buffer, format) {
|
|
486
|
+
const bytes = new Uint8Array(buffer);
|
|
487
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
488
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
489
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
490
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
491
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
492
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
493
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
494
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
495
|
+
let offset = tracks.dataStart;
|
|
496
|
+
let opusTrack;
|
|
497
|
+
while (offset < tracks.dataEnd) {
|
|
498
|
+
const track = readEbmlElement(bytes, offset);
|
|
499
|
+
if (track.id === 174) {
|
|
500
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
501
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
502
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
503
|
+
opusTrack = track;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
offset = track.dataEnd;
|
|
508
|
+
}
|
|
509
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
510
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
511
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
512
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
513
|
+
const sampleRate = sampling ? new DataView(
|
|
514
|
+
bytes.buffer,
|
|
515
|
+
bytes.byteOffset + sampling.dataStart,
|
|
516
|
+
sampling.dataEnd - sampling.dataStart
|
|
517
|
+
).getFloat64(0, false) : 0;
|
|
518
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
519
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
520
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
521
|
+
return {
|
|
522
|
+
format,
|
|
523
|
+
mimeType: "audio/webm",
|
|
524
|
+
codec: "opus",
|
|
525
|
+
sampleRate: Math.round(sampleRate),
|
|
526
|
+
channels: channelCount,
|
|
527
|
+
container: "webm",
|
|
528
|
+
isVbr: true,
|
|
529
|
+
isCompressed: true
|
|
530
|
+
};
|
|
531
|
+
}
|
|
296
532
|
function inspectAudioSpecification(buffer, format) {
|
|
297
533
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
298
534
|
const parsed = parseWav(buffer);
|
|
@@ -302,27 +538,78 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
302
538
|
const channels = view.getUint16(2, true);
|
|
303
539
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
304
540
|
const formatCode = view.getUint16(0, true);
|
|
541
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
542
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
305
543
|
return {
|
|
306
544
|
format,
|
|
307
545
|
mimeType: "audio/wav",
|
|
308
|
-
codec
|
|
546
|
+
codec,
|
|
309
547
|
sampleRate,
|
|
310
548
|
channels,
|
|
311
549
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
312
550
|
bitDepth: bitsPerSample,
|
|
313
551
|
container: "riff-wave",
|
|
314
552
|
isVbr: false,
|
|
315
|
-
isCompressed:
|
|
553
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
316
554
|
};
|
|
317
555
|
}
|
|
318
|
-
if (isMp3Format(format))
|
|
319
|
-
|
|
556
|
+
if (isMp3Format(format)) {
|
|
557
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
558
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
559
|
+
}
|
|
560
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
561
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
562
|
+
validateContainerFormat(specification2, format);
|
|
563
|
+
return specification2;
|
|
564
|
+
}
|
|
565
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
566
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
567
|
+
validateContainerFormat(specification2, format);
|
|
568
|
+
return specification2;
|
|
569
|
+
}
|
|
570
|
+
const specification = formatAudioSpecification(format);
|
|
571
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
572
|
+
return specification;
|
|
573
|
+
}
|
|
574
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
575
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
576
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
577
|
+
}
|
|
578
|
+
if (specification.codec === "siren") return;
|
|
579
|
+
if (specification.codec === "silk") {
|
|
580
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
581
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
585
|
+
if (specification.codec === "opus") {
|
|
586
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
587
|
+
const frameCountCode = packetCode & 3;
|
|
588
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
589
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
593
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function validateContainerFormat(specification, format) {
|
|
600
|
+
const expected = formatAudioSpecification(format);
|
|
601
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
602
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
603
|
+
expected,
|
|
604
|
+
specification
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
320
607
|
}
|
|
321
608
|
function validateAudioSpecifications(specs) {
|
|
322
609
|
const first = specs[0];
|
|
323
610
|
if (!first) return;
|
|
324
611
|
const mismatch = specs.find(
|
|
325
|
-
(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
|
|
612
|
+
(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
|
|
326
613
|
);
|
|
327
614
|
if (mismatch)
|
|
328
615
|
throw new AudioFormatMismatchError(
|
|
@@ -399,12 +686,67 @@ function stripMp3Tags(buffer) {
|
|
|
399
686
|
function isMp3Format(format) {
|
|
400
687
|
return /(?:mp3|mpeg)/i.test(format);
|
|
401
688
|
}
|
|
689
|
+
function isOggFormat(format) {
|
|
690
|
+
return /ogg/i.test(format);
|
|
691
|
+
}
|
|
692
|
+
function isWebmFormat(format) {
|
|
693
|
+
return /webm/i.test(format);
|
|
694
|
+
}
|
|
402
695
|
function isWavFormat(format) {
|
|
403
696
|
return /(?:wav|wave|riff)/i.test(format);
|
|
404
697
|
}
|
|
405
698
|
function isRawFormat(format) {
|
|
406
699
|
return /^raw(?:-|$)/i.test(format);
|
|
407
700
|
}
|
|
701
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
702
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
703
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
704
|
+
}
|
|
705
|
+
let specification;
|
|
706
|
+
try {
|
|
707
|
+
specification = inspectAudioSpecification(merged, format);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (!allowExternalContainer) throw error;
|
|
710
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
711
|
+
}
|
|
712
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
713
|
+
const firstInput = inputSpecs[0];
|
|
714
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
715
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
716
|
+
...inputSpecs,
|
|
717
|
+
specification
|
|
718
|
+
]);
|
|
719
|
+
}
|
|
720
|
+
if (isRawFormat(format)) {
|
|
721
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
722
|
+
if (merged.byteLength !== expectedSize) {
|
|
723
|
+
throw new MergeError(
|
|
724
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
validateRawAudioBuffer(merged, specification);
|
|
728
|
+
}
|
|
729
|
+
return specification;
|
|
730
|
+
}
|
|
731
|
+
async function withinDeadline(value, deadline) {
|
|
732
|
+
if (!deadline) return value;
|
|
733
|
+
deadline.throwIfExpired();
|
|
734
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
735
|
+
let timer;
|
|
736
|
+
try {
|
|
737
|
+
return await Promise.race([
|
|
738
|
+
Promise.resolve(value),
|
|
739
|
+
new Promise((_resolve, reject) => {
|
|
740
|
+
timer = setTimeout(
|
|
741
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
742
|
+
deadline.remainingMs
|
|
743
|
+
);
|
|
744
|
+
})
|
|
745
|
+
]);
|
|
746
|
+
} finally {
|
|
747
|
+
if (timer) clearTimeout(timer);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
408
750
|
function resolveMergeAudioFormat(format) {
|
|
409
751
|
if (isWavFormat(format)) return "wav";
|
|
410
752
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -417,6 +759,7 @@ function canMergeAudioFormat(format) {
|
|
|
417
759
|
function mergeAudioBuffers(buffers, options) {
|
|
418
760
|
const format = typeof options === "string" ? options : options?.format;
|
|
419
761
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
762
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
420
763
|
try {
|
|
421
764
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
422
765
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -457,7 +800,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
457
800
|
}
|
|
458
801
|
}
|
|
459
802
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
460
|
-
async function
|
|
803
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
461
804
|
if (config.signal?.aborted) {
|
|
462
805
|
throw new SynthesisCancelledError();
|
|
463
806
|
}
|
|
@@ -587,6 +930,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
587
930
|
rejectWithError(err);
|
|
588
931
|
return;
|
|
589
932
|
}
|
|
933
|
+
let audioSpec;
|
|
934
|
+
try {
|
|
935
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
rejectWithError(error);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
590
940
|
settled = true;
|
|
591
941
|
cleanup();
|
|
592
942
|
closeResources();
|
|
@@ -622,8 +972,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
622
972
|
resolve({
|
|
623
973
|
audioData: result.audioData,
|
|
624
974
|
durationMs,
|
|
625
|
-
audioSpec
|
|
626
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
975
|
+
audioSpec,
|
|
976
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
627
977
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
628
978
|
...requestId ? { requestId } : {},
|
|
629
979
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -636,7 +986,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
636
986
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
637
987
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
638
988
|
}
|
|
639
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
989
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
640
990
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
641
991
|
timeout = setTimeout(
|
|
642
992
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -689,7 +1039,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
689
1039
|
}
|
|
690
1040
|
});
|
|
691
1041
|
}
|
|
692
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
1042
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
693
1043
|
const options = retryOptions ? {
|
|
694
1044
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
695
1045
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -700,17 +1050,42 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
700
1050
|
while (true) {
|
|
701
1051
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
702
1052
|
try {
|
|
703
|
-
return await
|
|
1053
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
704
1054
|
} catch (error) {
|
|
705
1055
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
706
1056
|
throw error;
|
|
707
1057
|
attempt += 1;
|
|
708
1058
|
const delayMs = retryDelay(options, attempt, error);
|
|
1059
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1060
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
1061
|
+
throw new SynthesisTimeoutError(
|
|
1062
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
709
1065
|
onRetry(attempt, delayMs);
|
|
710
|
-
await waitForRetry(delayMs, config.signal);
|
|
1066
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
711
1067
|
}
|
|
712
1068
|
}
|
|
713
1069
|
}
|
|
1070
|
+
async function synthesizeSsml(ssml, config) {
|
|
1071
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1072
|
+
try {
|
|
1073
|
+
deadline.throwIfExpired();
|
|
1074
|
+
const synthesisConfig = {
|
|
1075
|
+
...config,
|
|
1076
|
+
signal: deadline.signal,
|
|
1077
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1078
|
+
};
|
|
1079
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1080
|
+
deadline.throwIfExpired();
|
|
1081
|
+
return result;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1084
|
+
throw error;
|
|
1085
|
+
} finally {
|
|
1086
|
+
deadline.dispose();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
714
1089
|
function createAbortScope(parent, timeoutMs) {
|
|
715
1090
|
const controller = new AbortController();
|
|
716
1091
|
let didTimeout = false;
|
|
@@ -731,10 +1106,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
731
1106
|
abort: () => controller.abort()
|
|
732
1107
|
};
|
|
733
1108
|
}
|
|
734
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
1109
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
735
1110
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
736
1111
|
try {
|
|
737
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
1112
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
738
1113
|
} catch (error) {
|
|
739
1114
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
740
1115
|
throw error;
|
|
@@ -743,18 +1118,42 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
743
1118
|
}
|
|
744
1119
|
}
|
|
745
1120
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
746
|
-
const results = new Array(chunks.length);
|
|
747
1121
|
const totalChunks = chunks.length;
|
|
1122
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1123
|
+
const fingerprints = inputs.map(
|
|
1124
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1125
|
+
region: config.region,
|
|
1126
|
+
endpoint: config.endpoint,
|
|
1127
|
+
customHeaders: config.customHeaders,
|
|
1128
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
const results = new Array(totalChunks);
|
|
748
1132
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1133
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1134
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1135
|
+
chunkIndex,
|
|
1136
|
+
status: "pending",
|
|
1137
|
+
canResume: true
|
|
1138
|
+
}));
|
|
749
1139
|
for (const [index, cached] of cachedChunks) {
|
|
750
|
-
if (index
|
|
1140
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
1141
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
1142
|
+
if (isValid) {
|
|
1143
|
+
results[index] = { ...cached };
|
|
1144
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1145
|
+
} else {
|
|
1146
|
+
invalidCachedIndices.add(index);
|
|
1147
|
+
}
|
|
751
1148
|
}
|
|
752
1149
|
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
753
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1150
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1151
|
+
const jobStartedAt = Date.now();
|
|
1152
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1153
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
754
1154
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
755
1155
|
const report = (event) => config.onProgress?.(event);
|
|
756
|
-
for (const [index,
|
|
757
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1156
|
+
for (const [index, input] of inputs.entries()) {
|
|
758
1157
|
report({
|
|
759
1158
|
currentChunk: index,
|
|
760
1159
|
totalChunks,
|
|
@@ -776,8 +1175,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
776
1175
|
if (index >= chunks.length) return;
|
|
777
1176
|
if (!shouldSynthesize(index)) continue;
|
|
778
1177
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
779
|
-
const
|
|
780
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1178
|
+
const input = inputs[index];
|
|
781
1179
|
report({
|
|
782
1180
|
currentChunk: completed,
|
|
783
1181
|
totalChunks,
|
|
@@ -793,7 +1191,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
793
1191
|
input.ssml,
|
|
794
1192
|
{
|
|
795
1193
|
...config,
|
|
796
|
-
signal:
|
|
1194
|
+
signal: deadline.signal,
|
|
797
1195
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
798
1196
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
799
1197
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -814,9 +1212,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
814
1212
|
retryAttempt,
|
|
815
1213
|
nextRetryDelayMs,
|
|
816
1214
|
isRetrying: true
|
|
817
|
-
})
|
|
1215
|
+
}),
|
|
1216
|
+
jobDeadlineAt
|
|
818
1217
|
);
|
|
819
1218
|
results[index] = result;
|
|
1219
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
820
1220
|
completed += 1;
|
|
821
1221
|
report({
|
|
822
1222
|
currentChunk: completed,
|
|
@@ -828,7 +1228,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
828
1228
|
durationMs: Date.now() - startedAt
|
|
829
1229
|
});
|
|
830
1230
|
} catch (error) {
|
|
831
|
-
|
|
1231
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
1232
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
1233
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1234
|
+
chunkStates[index] = {
|
|
1235
|
+
chunkIndex: index,
|
|
1236
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1237
|
+
isOriginalFailure: !wasCancelled,
|
|
1238
|
+
canResume: true,
|
|
1239
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1240
|
+
};
|
|
832
1241
|
report({
|
|
833
1242
|
currentChunk: completed,
|
|
834
1243
|
totalChunks,
|
|
@@ -839,7 +1248,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
839
1248
|
durationMs: Date.now() - startedAt,
|
|
840
1249
|
error
|
|
841
1250
|
});
|
|
842
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
843
1251
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
844
1252
|
return;
|
|
845
1253
|
}
|
|
@@ -848,26 +1256,47 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
848
1256
|
try {
|
|
849
1257
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
850
1258
|
if (firstError) throw firstError;
|
|
1259
|
+
const missingChunkIndices = Array.from(
|
|
1260
|
+
{ length: totalChunks },
|
|
1261
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1262
|
+
).filter((index) => index !== void 0);
|
|
1263
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
851
1264
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
852
1265
|
return await mergeSynthesisResults(orderedResults, {
|
|
853
1266
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
854
|
-
signal:
|
|
1267
|
+
signal: deadline.signal,
|
|
855
1268
|
customMerger: config.customMerger,
|
|
856
1269
|
outputMimeType: config.outputMimeType,
|
|
857
|
-
postMergeValidator: config.postMergeValidator
|
|
1270
|
+
postMergeValidator: config.postMergeValidator,
|
|
1271
|
+
deadline
|
|
858
1272
|
});
|
|
859
1273
|
} catch (error) {
|
|
1274
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
1275
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1276
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1277
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const synthesizedChunks = results.flatMap(
|
|
1282
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1283
|
+
);
|
|
860
1284
|
const partial = {
|
|
861
|
-
synthesizedChunks
|
|
862
|
-
completedChunks:
|
|
863
|
-
pendingChunkIndices:
|
|
1285
|
+
synthesizedChunks,
|
|
1286
|
+
completedChunks: synthesizedChunks,
|
|
1287
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1288
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1289
|
+
),
|
|
864
1290
|
failedChunkIndices: [...failedIndices],
|
|
1291
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1292
|
+
chunkStates,
|
|
865
1293
|
totalChunks
|
|
866
1294
|
};
|
|
867
1295
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
868
1296
|
throw error;
|
|
869
1297
|
} finally {
|
|
870
1298
|
scope.dispose();
|
|
1299
|
+
deadline.dispose();
|
|
871
1300
|
}
|
|
872
1301
|
}
|
|
873
1302
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -945,30 +1374,44 @@ function mergeSynthesisResults(results, options) {
|
|
|
945
1374
|
const format = resolvedOptions?.format;
|
|
946
1375
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
947
1376
|
const buffers = results.map((result) => result.audioData);
|
|
948
|
-
const inputSpecs = results.map((result) =>
|
|
1377
|
+
const inputSpecs = results.map((result) => {
|
|
1378
|
+
if (result.audioSpec) return result.audioSpec;
|
|
1379
|
+
try {
|
|
1380
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
949
1386
|
validateAudioSpecifications(inputSpecs);
|
|
950
|
-
const
|
|
1387
|
+
const deadline = resolvedOptions.deadline;
|
|
1388
|
+
deadline?.throwIfExpired();
|
|
1389
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
951
1390
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
952
1391
|
if (resolvedOptions.customMerger) {
|
|
953
|
-
return
|
|
954
|
-
()
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1392
|
+
return withinDeadline(
|
|
1393
|
+
Promise.resolve().then(
|
|
1394
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1395
|
+
format,
|
|
1396
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1397
|
+
inputSpecs,
|
|
1398
|
+
signal
|
|
1399
|
+
})
|
|
1400
|
+
),
|
|
1401
|
+
deadline
|
|
960
1402
|
).then((merged) => {
|
|
1403
|
+
deadline?.throwIfExpired();
|
|
961
1404
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
962
|
-
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
963
|
-
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
964
1405
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
965
|
-
const
|
|
966
|
-
results,
|
|
1406
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
967
1407
|
merged,
|
|
968
1408
|
format,
|
|
969
|
-
|
|
970
|
-
|
|
1409
|
+
buffers,
|
|
1410
|
+
inputSpecs,
|
|
1411
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1412
|
+
true
|
|
971
1413
|
);
|
|
1414
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
972
1415
|
return Promise.resolve(
|
|
973
1416
|
resolvedOptions.postMergeValidator?.(result, {
|
|
974
1417
|
format,
|
|
@@ -977,6 +1420,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
977
1420
|
signal
|
|
978
1421
|
})
|
|
979
1422
|
).then((valid) => {
|
|
1423
|
+
deadline?.throwIfExpired();
|
|
980
1424
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
981
1425
|
return result;
|
|
982
1426
|
});
|
|
@@ -987,6 +1431,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
987
1431
|
});
|
|
988
1432
|
}
|
|
989
1433
|
try {
|
|
1434
|
+
deadline?.throwIfExpired();
|
|
990
1435
|
const result = createMergedResult(
|
|
991
1436
|
results,
|
|
992
1437
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -1002,12 +1447,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
1002
1447
|
signal
|
|
1003
1448
|
});
|
|
1004
1449
|
if (validation instanceof Promise)
|
|
1005
|
-
return validation.then((valid) => {
|
|
1450
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
1451
|
+
deadline?.throwIfExpired();
|
|
1006
1452
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1007
1453
|
return result;
|
|
1008
1454
|
});
|
|
1009
1455
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1010
1456
|
}
|
|
1457
|
+
deadline?.throwIfExpired();
|
|
1011
1458
|
return result;
|
|
1012
1459
|
} catch (error) {
|
|
1013
1460
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -1101,7 +1548,7 @@ function resolveConcurrency2(value, total) {
|
|
|
1101
1548
|
if (value === Infinity) return Math.max(1, total);
|
|
1102
1549
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1103
1550
|
}
|
|
1104
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1551
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1105
1552
|
const retry = options ? {
|
|
1106
1553
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1107
1554
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -1118,6 +1565,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
1118
1565
|
throw error;
|
|
1119
1566
|
attempt += 1;
|
|
1120
1567
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1568
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1569
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1570
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1571
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1572
|
+
}
|
|
1121
1573
|
onRetry(attempt, delayMs);
|
|
1122
1574
|
if (delayMs > 0)
|
|
1123
1575
|
await new Promise((resolve, reject) => {
|
|
@@ -1151,14 +1603,19 @@ function sharedValidationOptions(options, signal) {
|
|
|
1151
1603
|
};
|
|
1152
1604
|
}
|
|
1153
1605
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1154
|
-
const
|
|
1606
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1607
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1155
1608
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
1156
|
-
if (
|
|
1157
|
-
const error = toSynthesisError(
|
|
1609
|
+
if (deadline.signal.aborted) {
|
|
1610
|
+
const error = toSynthesisError(
|
|
1611
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1612
|
+
);
|
|
1613
|
+
deadline.dispose();
|
|
1158
1614
|
return failure(error);
|
|
1159
1615
|
}
|
|
1160
1616
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1161
1617
|
if (errors.length > 0) {
|
|
1618
|
+
deadline.dispose();
|
|
1162
1619
|
return failure({
|
|
1163
1620
|
kind: "validation-error",
|
|
1164
1621
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
@@ -1171,23 +1628,30 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
1171
1628
|
success: true,
|
|
1172
1629
|
status: "success",
|
|
1173
1630
|
value: await client.synthesizeSsml(ssml, {
|
|
1174
|
-
signal:
|
|
1631
|
+
signal: deadline.signal,
|
|
1175
1632
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1176
|
-
timeouts: options.timeouts
|
|
1633
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1177
1634
|
})
|
|
1178
1635
|
};
|
|
1179
1636
|
} catch (error) {
|
|
1637
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1180
1638
|
const synthesisError = toSynthesisError(error);
|
|
1181
1639
|
return failure(synthesisError);
|
|
1640
|
+
} finally {
|
|
1641
|
+
deadline.dispose();
|
|
1182
1642
|
}
|
|
1183
1643
|
}
|
|
1184
1644
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1645
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1185
1646
|
const validationOptions = sharedValidationOptions(
|
|
1186
1647
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1187
|
-
|
|
1648
|
+
deadline.signal
|
|
1188
1649
|
);
|
|
1189
|
-
if (
|
|
1190
|
-
const error = toSynthesisError(
|
|
1650
|
+
if (deadline.signal.aborted) {
|
|
1651
|
+
const error = toSynthesisError(
|
|
1652
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1653
|
+
);
|
|
1654
|
+
deadline.dispose();
|
|
1191
1655
|
return failure(error);
|
|
1192
1656
|
}
|
|
1193
1657
|
const pending = (index, status, error) => {
|
|
@@ -1219,14 +1683,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1219
1683
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1220
1684
|
})
|
|
1221
1685
|
);
|
|
1222
|
-
if (
|
|
1686
|
+
if (deadline.signal.aborted) {
|
|
1223
1687
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1688
|
+
deadline.dispose();
|
|
1224
1689
|
return failure(error);
|
|
1225
1690
|
}
|
|
1226
1691
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1227
1692
|
if (chunkDiagnostics.length > 0) {
|
|
1228
1693
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1229
1694
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1695
|
+
deadline.dispose();
|
|
1230
1696
|
return failure(error);
|
|
1231
1697
|
}
|
|
1232
1698
|
let fallbackJobScope;
|
|
@@ -1239,9 +1705,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1239
1705
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1240
1706
|
onProgress: options.onProgress,
|
|
1241
1707
|
outputFormat: options.outputFormat,
|
|
1242
|
-
signal:
|
|
1708
|
+
signal: deadline.signal,
|
|
1243
1709
|
timeoutMs: options.timeoutMs,
|
|
1244
|
-
timeouts: options.timeouts,
|
|
1710
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1245
1711
|
sourceNodePath: options.sourceNodePath,
|
|
1246
1712
|
concurrency: options.concurrency,
|
|
1247
1713
|
retryOptions: options.retryOptions,
|
|
@@ -1250,18 +1716,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1250
1716
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1251
1717
|
customMerger: options.customMerger,
|
|
1252
1718
|
outputMimeType: options.outputMimeType,
|
|
1253
|
-
postMergeValidator: options.postMergeValidator
|
|
1719
|
+
postMergeValidator: options.postMergeValidator,
|
|
1720
|
+
resumeValidation: options.resumeValidation,
|
|
1721
|
+
customHeaders: options.customHeaders,
|
|
1722
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1254
1723
|
});
|
|
1255
1724
|
return { ok: true, success: true, status: "success", value };
|
|
1256
1725
|
}
|
|
1726
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1727
|
+
const fingerprints = inputs.map(
|
|
1728
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1729
|
+
customHeaders: options.customHeaders,
|
|
1730
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1731
|
+
})
|
|
1732
|
+
);
|
|
1257
1733
|
const results = new Array(chunks.length);
|
|
1734
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1735
|
+
chunkIndex,
|
|
1736
|
+
status: "pending",
|
|
1737
|
+
canResume: true
|
|
1738
|
+
}));
|
|
1258
1739
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1740
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1259
1741
|
for (const [index, cached] of cachedChunks) {
|
|
1260
|
-
if (index
|
|
1742
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
1743
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1744
|
+
results[index] = cached;
|
|
1745
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1746
|
+
} else invalidCachedIndices.add(index);
|
|
1261
1747
|
}
|
|
1262
1748
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1263
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1264
|
-
const
|
|
1749
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1750
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1751
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1265
1752
|
fallbackJobScope = jobScope;
|
|
1266
1753
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
1267
1754
|
let firstError;
|
|
@@ -1273,7 +1760,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1273
1760
|
const index = nextIndex++;
|
|
1274
1761
|
if (index >= chunks.length) return;
|
|
1275
1762
|
if (!shouldSynthesize(index)) continue;
|
|
1276
|
-
if (
|
|
1763
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1764
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1277
1767
|
const chunk = chunks[index];
|
|
1278
1768
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1279
1769
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -1282,8 +1772,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1282
1772
|
const startedAt = Date.now();
|
|
1283
1773
|
try {
|
|
1284
1774
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1285
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
1286
|
-
const chunkSignal = chunkScope?.signal ??
|
|
1775
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1776
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1287
1777
|
let result;
|
|
1288
1778
|
try {
|
|
1289
1779
|
result = await retryableSynthesis(
|
|
@@ -1306,10 +1796,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1306
1796
|
retryAttempt,
|
|
1307
1797
|
nextRetryDelayMs,
|
|
1308
1798
|
isRetrying: true
|
|
1309
|
-
})
|
|
1799
|
+
}),
|
|
1800
|
+
jobDeadlineAt
|
|
1310
1801
|
);
|
|
1311
1802
|
} catch (error) {
|
|
1312
|
-
if (chunkScope?.timedOut())
|
|
1803
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1313
1804
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1314
1805
|
throw error;
|
|
1315
1806
|
} finally {
|
|
@@ -1358,6 +1849,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1358
1849
|
}))
|
|
1359
1850
|
} : {}
|
|
1360
1851
|
};
|
|
1852
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1361
1853
|
completed += 1;
|
|
1362
1854
|
options.onProgress?.({
|
|
1363
1855
|
currentChunk: completed,
|
|
@@ -1369,7 +1861,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1369
1861
|
durationMs: Date.now() - startedAt
|
|
1370
1862
|
});
|
|
1371
1863
|
} catch (error) {
|
|
1372
|
-
|
|
1864
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1865
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1866
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1867
|
+
chunkStates[index] = {
|
|
1868
|
+
chunkIndex: index,
|
|
1869
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1870
|
+
isOriginalFailure: !wasCancelled,
|
|
1871
|
+
canResume: true,
|
|
1872
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1873
|
+
};
|
|
1373
1874
|
options.onProgress?.({
|
|
1374
1875
|
currentChunk: completed,
|
|
1375
1876
|
totalChunks: chunks.length,
|
|
@@ -1381,23 +1882,41 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1381
1882
|
error
|
|
1382
1883
|
});
|
|
1383
1884
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1384
|
-
firstError ?? (firstError = error);
|
|
1385
1885
|
return;
|
|
1386
1886
|
}
|
|
1387
1887
|
}
|
|
1388
1888
|
};
|
|
1389
1889
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1890
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1891
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1892
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1893
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1390
1897
|
if (failedIndices.size > 0) {
|
|
1391
1898
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1899
|
+
const synthesizedChunks = results.flatMap(
|
|
1900
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1901
|
+
);
|
|
1392
1902
|
error.partialResult = {
|
|
1393
|
-
synthesizedChunks
|
|
1394
|
-
completedChunks:
|
|
1395
|
-
pendingChunkIndices:
|
|
1903
|
+
synthesizedChunks,
|
|
1904
|
+
completedChunks: synthesizedChunks,
|
|
1905
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1906
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1907
|
+
),
|
|
1396
1908
|
failedChunkIndices: [...failedIndices],
|
|
1909
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1910
|
+
chunkStates,
|
|
1397
1911
|
totalChunks: chunks.length
|
|
1398
1912
|
};
|
|
1399
1913
|
throw error;
|
|
1400
1914
|
}
|
|
1915
|
+
const missingChunkIndices = Array.from(
|
|
1916
|
+
{ length: chunks.length },
|
|
1917
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1918
|
+
).filter((index) => index !== void 0);
|
|
1919
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1401
1920
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1402
1921
|
return {
|
|
1403
1922
|
ok: true,
|
|
@@ -1405,7 +1924,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1405
1924
|
status: "success",
|
|
1406
1925
|
value: await mergeSynthesisResults(orderedResults, {
|
|
1407
1926
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1408
|
-
signal: jobScope?.signal ??
|
|
1927
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
1409
1928
|
customMerger: options.customMerger,
|
|
1410
1929
|
outputMimeType: options.outputMimeType,
|
|
1411
1930
|
postMergeValidator: options.postMergeValidator
|
|
@@ -1416,6 +1935,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1416
1935
|
return failure(synthesisError, partialResultFrom(error));
|
|
1417
1936
|
} finally {
|
|
1418
1937
|
fallbackJobScope?.dispose();
|
|
1938
|
+
deadline.dispose();
|
|
1419
1939
|
}
|
|
1420
1940
|
}
|
|
1421
1941
|
function withValidationSignal(options, signal) {
|
|
@@ -1436,14 +1956,43 @@ var AzureTtsClient = class {
|
|
|
1436
1956
|
__privateSet(this, _options, options);
|
|
1437
1957
|
}
|
|
1438
1958
|
async synthesize(ssml) {
|
|
1439
|
-
const {
|
|
1959
|
+
const {
|
|
1960
|
+
region,
|
|
1961
|
+
subscriptionKey,
|
|
1962
|
+
outputFormat,
|
|
1963
|
+
signal,
|
|
1964
|
+
timeoutMs,
|
|
1965
|
+
timeouts,
|
|
1966
|
+
customHeaders,
|
|
1967
|
+
fingerprintSchemaVersion
|
|
1968
|
+
} = __privateGet(this, _options);
|
|
1440
1969
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1441
1970
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1442
|
-
const config = {
|
|
1971
|
+
const config = {
|
|
1972
|
+
endpoint,
|
|
1973
|
+
region,
|
|
1974
|
+
subscriptionKey,
|
|
1975
|
+
outputFormat,
|
|
1976
|
+
signal,
|
|
1977
|
+
timeoutMs,
|
|
1978
|
+
timeouts,
|
|
1979
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
1980
|
+
customHeaders,
|
|
1981
|
+
fingerprintSchemaVersion
|
|
1982
|
+
};
|
|
1443
1983
|
return synthesizeSpeech(ssml, config);
|
|
1444
1984
|
}
|
|
1445
1985
|
async synthesizeSsml(ssml, options = {}) {
|
|
1446
|
-
const {
|
|
1986
|
+
const {
|
|
1987
|
+
region,
|
|
1988
|
+
subscriptionKey,
|
|
1989
|
+
outputFormat,
|
|
1990
|
+
signal,
|
|
1991
|
+
timeoutMs,
|
|
1992
|
+
timeouts,
|
|
1993
|
+
customHeaders,
|
|
1994
|
+
fingerprintSchemaVersion
|
|
1995
|
+
} = __privateGet(this, _options);
|
|
1447
1996
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1448
1997
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1449
1998
|
return synthesizeSsml(ssml, {
|
|
@@ -1456,11 +2005,28 @@ var AzureTtsClient = class {
|
|
|
1456
2005
|
timeouts: options.timeouts ?? timeouts,
|
|
1457
2006
|
sourceNodePath: options.sourceNodePath,
|
|
1458
2007
|
sourceTextSegments: options.sourceTextSegments,
|
|
1459
|
-
sourceMarkers: options.sourceMarkers
|
|
2008
|
+
sourceMarkers: options.sourceMarkers,
|
|
2009
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2010
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2011
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2012
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2013
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2014
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2015
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2016
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1460
2017
|
});
|
|
1461
2018
|
}
|
|
1462
2019
|
async synthesizeChunks(chunks, options = {}) {
|
|
1463
|
-
const {
|
|
2020
|
+
const {
|
|
2021
|
+
region,
|
|
2022
|
+
subscriptionKey,
|
|
2023
|
+
outputFormat,
|
|
2024
|
+
signal,
|
|
2025
|
+
timeoutMs,
|
|
2026
|
+
timeouts,
|
|
2027
|
+
customHeaders,
|
|
2028
|
+
fingerprintSchemaVersion
|
|
2029
|
+
} = __privateGet(this, _options);
|
|
1464
2030
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1465
2031
|
return synthesizeSsmlChunks(chunks, {
|
|
1466
2032
|
endpoint,
|
|
@@ -1474,12 +2040,15 @@ var AzureTtsClient = class {
|
|
|
1474
2040
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1475
2041
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1476
2042
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1477
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
2043
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1478
2044
|
resumeChunks: options.resumeChunks,
|
|
1479
2045
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1480
|
-
customMerger: options.customMerger,
|
|
1481
|
-
outputMimeType: options.outputMimeType,
|
|
1482
|
-
postMergeValidator: options.postMergeValidator
|
|
2046
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2047
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2048
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2049
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2050
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2051
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1483
2052
|
});
|
|
1484
2053
|
}
|
|
1485
2054
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1494,7 +2063,14 @@ var AzureTtsClient = class {
|
|
|
1494
2063
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1495
2064
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1496
2065
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1497
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
2066
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2067
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2068
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2069
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2070
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2071
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2072
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2073
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
1498
2074
|
});
|
|
1499
2075
|
}
|
|
1500
2076
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1599,11 +2175,14 @@ export {
|
|
|
1599
2175
|
BatchChunkValidationError,
|
|
1600
2176
|
ChunkValidationError,
|
|
1601
2177
|
DEFAULT_OUTPUT_FORMAT,
|
|
2178
|
+
DeadlineController,
|
|
2179
|
+
IncompleteChunkSetError,
|
|
1602
2180
|
MergeError,
|
|
1603
2181
|
SynthesisCancelledError,
|
|
1604
2182
|
SynthesisTimeoutError,
|
|
1605
2183
|
UnsupportedMergeFormatError,
|
|
1606
2184
|
canMergeAudioFormat,
|
|
2185
|
+
computeChunkFingerprint,
|
|
1607
2186
|
fetchAzureVoiceCatalog,
|
|
1608
2187
|
getRetryAfterDelayMs,
|
|
1609
2188
|
inspectAudioSpecification,
|
|
@@ -1611,6 +2190,7 @@ export {
|
|
|
1611
2190
|
mergeSynthesisResults,
|
|
1612
2191
|
resolveMergeAudioFormat,
|
|
1613
2192
|
resolveMimeType,
|
|
2193
|
+
serializeChunkError,
|
|
1614
2194
|
synthesizeSpeech,
|
|
1615
2195
|
synthesizeSsml,
|
|
1616
2196
|
synthesizeSsmlChunks,
|