ssml-builder-js 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/README.md +8 -0
- package/dist/{index.d-tpKoP1jl.d.mts → index.d-4kqVuH29.d.mts} +1 -1
- package/dist/{index.d-tpKoP1jl.d.ts → index.d-4kqVuH29.d.ts} +1 -1
- package/dist/index.d.mts +124 -44
- package/dist/index.d.ts +124 -44
- 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/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +45 -24
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +45 -24
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -93,6 +93,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
93
93
|
this.name = "SynthesisTimeoutError";
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
+
var IncompleteChunkSetError = class extends Error {
|
|
97
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
98
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
99
|
+
this.kind = "incomplete-chunk-set";
|
|
100
|
+
this.name = "IncompleteChunkSetError";
|
|
101
|
+
this.totalChunks = totalChunks;
|
|
102
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
96
105
|
var MergeError = class extends Error {
|
|
97
106
|
constructor(message, cause) {
|
|
98
107
|
super(message);
|
|
@@ -117,8 +126,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
117
126
|
this.format = format;
|
|
118
127
|
}
|
|
119
128
|
};
|
|
129
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
132
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
133
|
+
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";
|
|
134
|
+
const details = {};
|
|
135
|
+
if (error instanceof AzureTtsError) {
|
|
136
|
+
details.statusText = error.statusText;
|
|
137
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
138
|
+
}
|
|
139
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
140
|
+
details.totalChunks = error.totalChunks;
|
|
141
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
code,
|
|
145
|
+
phase,
|
|
146
|
+
message,
|
|
147
|
+
isOriginalFailure,
|
|
148
|
+
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)),
|
|
149
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
150
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
120
153
|
function toSynthesisError(error) {
|
|
121
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
154
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
122
155
|
return error;
|
|
123
156
|
const message = error instanceof Error ? error.message : String(error);
|
|
124
157
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -130,6 +163,55 @@ function createSpeechSdkError(error) {
|
|
|
130
163
|
return new AzureTtsSdkError(message);
|
|
131
164
|
}
|
|
132
165
|
|
|
166
|
+
// packages/azure-tts-client/src/deadline.ts
|
|
167
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
168
|
+
var DeadlineController = class {
|
|
169
|
+
constructor(totalJobMs, parent) {
|
|
170
|
+
__privateAdd(this, _controller, new AbortController());
|
|
171
|
+
__privateAdd(this, _parent);
|
|
172
|
+
__privateAdd(this, _onParentAbort);
|
|
173
|
+
__privateAdd(this, _timer);
|
|
174
|
+
__privateAdd(this, _timedOut, false);
|
|
175
|
+
__privateSet(this, _parent, parent);
|
|
176
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
177
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
178
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
179
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
180
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
181
|
+
if (this.deadlineAtMs !== void 0) {
|
|
182
|
+
__privateSet(this, _timer, setTimeout(
|
|
183
|
+
() => {
|
|
184
|
+
__privateSet(this, _timedOut, true);
|
|
185
|
+
__privateGet(this, _controller).abort();
|
|
186
|
+
},
|
|
187
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
get timedOut() {
|
|
192
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
193
|
+
}
|
|
194
|
+
get remainingMs() {
|
|
195
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
196
|
+
}
|
|
197
|
+
throwIfExpired() {
|
|
198
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
199
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
200
|
+
}
|
|
201
|
+
abort() {
|
|
202
|
+
__privateGet(this, _controller).abort();
|
|
203
|
+
}
|
|
204
|
+
dispose() {
|
|
205
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
206
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
_controller = new WeakMap();
|
|
210
|
+
_parent = new WeakMap();
|
|
211
|
+
_onParentAbort = new WeakMap();
|
|
212
|
+
_timer = new WeakMap();
|
|
213
|
+
_timedOut = new WeakMap();
|
|
214
|
+
|
|
133
215
|
// packages/azure-tts-client/src/synthesis.ts
|
|
134
216
|
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
135
217
|
|
|
@@ -180,6 +262,9 @@ var OUTPUT_FORMATS = {
|
|
|
180
262
|
function resolveMimeType(outputFormat) {
|
|
181
263
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
182
264
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
265
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
266
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
267
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
183
268
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
184
269
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
185
270
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -208,6 +293,32 @@ function createSpeechConfig(config) {
|
|
|
208
293
|
}
|
|
209
294
|
|
|
210
295
|
// packages/azure-tts-client/src/synthesis.ts
|
|
296
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
297
|
+
const readAttribute = (name) => {
|
|
298
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
299
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
300
|
+
};
|
|
301
|
+
const headers = Object.fromEntries(
|
|
302
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
303
|
+
);
|
|
304
|
+
const payload = JSON.stringify({
|
|
305
|
+
ssml,
|
|
306
|
+
outputFormat,
|
|
307
|
+
region: options.region ?? "",
|
|
308
|
+
endpoint: options.endpoint ?? "",
|
|
309
|
+
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
310
|
+
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
311
|
+
customHeaders: headers,
|
|
312
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
313
|
+
});
|
|
314
|
+
let hash = 0xcbf29ce484222325n;
|
|
315
|
+
const mask = 0xffffffffffffffffn;
|
|
316
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
317
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
318
|
+
hash = hash * 0x100000001b3n & mask;
|
|
319
|
+
}
|
|
320
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
321
|
+
}
|
|
211
322
|
function ascii(bytes, offset, value) {
|
|
212
323
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
213
324
|
}
|
|
@@ -247,9 +358,11 @@ function parseWav(buffer) {
|
|
|
247
358
|
}
|
|
248
359
|
return { chunks, data, format };
|
|
249
360
|
}
|
|
250
|
-
function
|
|
251
|
-
const match =
|
|
252
|
-
|
|
361
|
+
function formatSampleRate(format) {
|
|
362
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
363
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
364
|
+
const value = Number(match[1]);
|
|
365
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
253
366
|
}
|
|
254
367
|
function formatChannels(format, fallback) {
|
|
255
368
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -257,11 +370,11 @@ function formatChannels(format, fallback) {
|
|
|
257
370
|
return fallback;
|
|
258
371
|
}
|
|
259
372
|
function formatAudioSpecification(format) {
|
|
260
|
-
const sampleRate =
|
|
373
|
+
const sampleRate = formatSampleRate(format);
|
|
261
374
|
const channels = formatChannels(format, 0);
|
|
262
375
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
263
376
|
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
264
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /
|
|
377
|
+
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";
|
|
265
378
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
266
379
|
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;
|
|
267
380
|
return {
|
|
@@ -274,7 +387,7 @@ function formatAudioSpecification(format) {
|
|
|
274
387
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
275
388
|
...container ? { container } : {},
|
|
276
389
|
isVbr: /vbr/i.test(format),
|
|
277
|
-
isCompressed: codec
|
|
390
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
278
391
|
};
|
|
279
392
|
}
|
|
280
393
|
function parseMp3Specification(buffer, format) {
|
|
@@ -316,6 +429,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
316
429
|
}
|
|
317
430
|
return void 0;
|
|
318
431
|
}
|
|
432
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
433
|
+
const first = bytes[offset];
|
|
434
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
435
|
+
let mask = 128;
|
|
436
|
+
let length = 1;
|
|
437
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
438
|
+
mask >>= 1;
|
|
439
|
+
length += 1;
|
|
440
|
+
}
|
|
441
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
442
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
443
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
444
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
445
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
446
|
+
return { value, length };
|
|
447
|
+
}
|
|
448
|
+
function readEbmlElement(bytes, offset) {
|
|
449
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
450
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
451
|
+
const dataStart = offset + id.length + size.length;
|
|
452
|
+
const dataEnd = dataStart + size.value;
|
|
453
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
454
|
+
return { id: id.value, dataStart, dataEnd };
|
|
455
|
+
}
|
|
456
|
+
function ebmlText(bytes, element) {
|
|
457
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
458
|
+
}
|
|
459
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
460
|
+
let offset = start;
|
|
461
|
+
while (offset < end) {
|
|
462
|
+
const element = readEbmlElement(bytes, offset);
|
|
463
|
+
if (element.id === id) return element;
|
|
464
|
+
offset = element.dataEnd;
|
|
465
|
+
}
|
|
466
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
function parseOggSpecification(buffer, format) {
|
|
470
|
+
const bytes = new Uint8Array(buffer);
|
|
471
|
+
let offset = 0;
|
|
472
|
+
let firstPayload;
|
|
473
|
+
let pages = 0;
|
|
474
|
+
while (offset < bytes.byteLength) {
|
|
475
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
476
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
477
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
478
|
+
const lacingStart = offset + 27;
|
|
479
|
+
const payloadStart = lacingStart + segmentCount;
|
|
480
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
481
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
482
|
+
const pageEnd = payloadStart + payloadLength;
|
|
483
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
484
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
485
|
+
offset = pageEnd;
|
|
486
|
+
pages += 1;
|
|
487
|
+
}
|
|
488
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
489
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
490
|
+
const version = firstPayload[8];
|
|
491
|
+
const channels = firstPayload[9] ?? 0;
|
|
492
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
493
|
+
12,
|
|
494
|
+
true
|
|
495
|
+
);
|
|
496
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
497
|
+
return {
|
|
498
|
+
format,
|
|
499
|
+
mimeType: "audio/ogg",
|
|
500
|
+
codec: "opus",
|
|
501
|
+
sampleRate,
|
|
502
|
+
channels,
|
|
503
|
+
container: "ogg",
|
|
504
|
+
isVbr: true,
|
|
505
|
+
isCompressed: true
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function parseWebmSpecification(buffer, format) {
|
|
509
|
+
const bytes = new Uint8Array(buffer);
|
|
510
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
511
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
512
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
513
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
514
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
515
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
516
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
517
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
518
|
+
let offset = tracks.dataStart;
|
|
519
|
+
let opusTrack;
|
|
520
|
+
while (offset < tracks.dataEnd) {
|
|
521
|
+
const track = readEbmlElement(bytes, offset);
|
|
522
|
+
if (track.id === 174) {
|
|
523
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
524
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
525
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
526
|
+
opusTrack = track;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
offset = track.dataEnd;
|
|
531
|
+
}
|
|
532
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
533
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
534
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
535
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
536
|
+
const sampleRate = sampling ? new DataView(
|
|
537
|
+
bytes.buffer,
|
|
538
|
+
bytes.byteOffset + sampling.dataStart,
|
|
539
|
+
sampling.dataEnd - sampling.dataStart
|
|
540
|
+
).getFloat64(0, false) : 0;
|
|
541
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
542
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
543
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
544
|
+
return {
|
|
545
|
+
format,
|
|
546
|
+
mimeType: "audio/webm",
|
|
547
|
+
codec: "opus",
|
|
548
|
+
sampleRate: Math.round(sampleRate),
|
|
549
|
+
channels: channelCount,
|
|
550
|
+
container: "webm",
|
|
551
|
+
isVbr: true,
|
|
552
|
+
isCompressed: true
|
|
553
|
+
};
|
|
554
|
+
}
|
|
319
555
|
function inspectAudioSpecification(buffer, format) {
|
|
320
556
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
321
557
|
const parsed = parseWav(buffer);
|
|
@@ -325,27 +561,78 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
325
561
|
const channels = view.getUint16(2, true);
|
|
326
562
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
327
563
|
const formatCode = view.getUint16(0, true);
|
|
564
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
565
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
328
566
|
return {
|
|
329
567
|
format,
|
|
330
568
|
mimeType: "audio/wav",
|
|
331
|
-
codec
|
|
569
|
+
codec,
|
|
332
570
|
sampleRate,
|
|
333
571
|
channels,
|
|
334
572
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
335
573
|
bitDepth: bitsPerSample,
|
|
336
574
|
container: "riff-wave",
|
|
337
575
|
isVbr: false,
|
|
338
|
-
isCompressed:
|
|
576
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
339
577
|
};
|
|
340
578
|
}
|
|
341
|
-
if (isMp3Format(format))
|
|
342
|
-
|
|
579
|
+
if (isMp3Format(format)) {
|
|
580
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
581
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
582
|
+
}
|
|
583
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
584
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
585
|
+
validateContainerFormat(specification2, format);
|
|
586
|
+
return specification2;
|
|
587
|
+
}
|
|
588
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
589
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
590
|
+
validateContainerFormat(specification2, format);
|
|
591
|
+
return specification2;
|
|
592
|
+
}
|
|
593
|
+
const specification = formatAudioSpecification(format);
|
|
594
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
595
|
+
return specification;
|
|
596
|
+
}
|
|
597
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
598
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
599
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
600
|
+
}
|
|
601
|
+
if (specification.codec === "siren") return;
|
|
602
|
+
if (specification.codec === "silk") {
|
|
603
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
604
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
608
|
+
if (specification.codec === "opus") {
|
|
609
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
610
|
+
const frameCountCode = packetCode & 3;
|
|
611
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
612
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
616
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function validateContainerFormat(specification, format) {
|
|
623
|
+
const expected = formatAudioSpecification(format);
|
|
624
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
625
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
626
|
+
expected,
|
|
627
|
+
specification
|
|
628
|
+
]);
|
|
629
|
+
}
|
|
343
630
|
}
|
|
344
631
|
function validateAudioSpecifications(specs) {
|
|
345
632
|
const first = specs[0];
|
|
346
633
|
if (!first) return;
|
|
347
634
|
const mismatch = specs.find(
|
|
348
|
-
(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
|
|
635
|
+
(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
|
|
349
636
|
);
|
|
350
637
|
if (mismatch)
|
|
351
638
|
throw new AudioFormatMismatchError(
|
|
@@ -422,12 +709,67 @@ function stripMp3Tags(buffer) {
|
|
|
422
709
|
function isMp3Format(format) {
|
|
423
710
|
return /(?:mp3|mpeg)/i.test(format);
|
|
424
711
|
}
|
|
712
|
+
function isOggFormat(format) {
|
|
713
|
+
return /ogg/i.test(format);
|
|
714
|
+
}
|
|
715
|
+
function isWebmFormat(format) {
|
|
716
|
+
return /webm/i.test(format);
|
|
717
|
+
}
|
|
425
718
|
function isWavFormat(format) {
|
|
426
719
|
return /(?:wav|wave|riff)/i.test(format);
|
|
427
720
|
}
|
|
428
721
|
function isRawFormat(format) {
|
|
429
722
|
return /^raw(?:-|$)/i.test(format);
|
|
430
723
|
}
|
|
724
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
725
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
726
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
727
|
+
}
|
|
728
|
+
let specification;
|
|
729
|
+
try {
|
|
730
|
+
specification = inspectAudioSpecification(merged, format);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (!allowExternalContainer) throw error;
|
|
733
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
734
|
+
}
|
|
735
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
736
|
+
const firstInput = inputSpecs[0];
|
|
737
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
738
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
739
|
+
...inputSpecs,
|
|
740
|
+
specification
|
|
741
|
+
]);
|
|
742
|
+
}
|
|
743
|
+
if (isRawFormat(format)) {
|
|
744
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
745
|
+
if (merged.byteLength !== expectedSize) {
|
|
746
|
+
throw new MergeError(
|
|
747
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
validateRawAudioBuffer(merged, specification);
|
|
751
|
+
}
|
|
752
|
+
return specification;
|
|
753
|
+
}
|
|
754
|
+
async function withinDeadline(value, deadline) {
|
|
755
|
+
if (!deadline) return value;
|
|
756
|
+
deadline.throwIfExpired();
|
|
757
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
758
|
+
let timer;
|
|
759
|
+
try {
|
|
760
|
+
return await Promise.race([
|
|
761
|
+
Promise.resolve(value),
|
|
762
|
+
new Promise((_resolve, reject) => {
|
|
763
|
+
timer = setTimeout(
|
|
764
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
765
|
+
deadline.remainingMs
|
|
766
|
+
);
|
|
767
|
+
})
|
|
768
|
+
]);
|
|
769
|
+
} finally {
|
|
770
|
+
if (timer) clearTimeout(timer);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
431
773
|
function resolveMergeAudioFormat(format) {
|
|
432
774
|
if (isWavFormat(format)) return "wav";
|
|
433
775
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -440,6 +782,7 @@ function canMergeAudioFormat(format) {
|
|
|
440
782
|
function mergeAudioBuffers(buffers, options) {
|
|
441
783
|
const format = typeof options === "string" ? options : options?.format;
|
|
442
784
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
785
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
443
786
|
try {
|
|
444
787
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
445
788
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -480,7 +823,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
480
823
|
}
|
|
481
824
|
}
|
|
482
825
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
483
|
-
async function
|
|
826
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
484
827
|
if (config.signal?.aborted) {
|
|
485
828
|
throw new SynthesisCancelledError();
|
|
486
829
|
}
|
|
@@ -610,6 +953,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
610
953
|
rejectWithError(err);
|
|
611
954
|
return;
|
|
612
955
|
}
|
|
956
|
+
let audioSpec;
|
|
957
|
+
try {
|
|
958
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
rejectWithError(error);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
613
963
|
settled = true;
|
|
614
964
|
cleanup();
|
|
615
965
|
closeResources();
|
|
@@ -645,8 +995,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
645
995
|
resolve({
|
|
646
996
|
audioData: result.audioData,
|
|
647
997
|
durationMs,
|
|
648
|
-
audioSpec
|
|
649
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
998
|
+
audioSpec,
|
|
999
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
650
1000
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
651
1001
|
...requestId ? { requestId } : {},
|
|
652
1002
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -659,7 +1009,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
659
1009
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
660
1010
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
661
1011
|
}
|
|
662
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
1012
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
663
1013
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
664
1014
|
timeout = setTimeout(
|
|
665
1015
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -712,7 +1062,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
712
1062
|
}
|
|
713
1063
|
});
|
|
714
1064
|
}
|
|
715
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
1065
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
716
1066
|
const options = retryOptions ? {
|
|
717
1067
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
718
1068
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -723,17 +1073,42 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
723
1073
|
while (true) {
|
|
724
1074
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
725
1075
|
try {
|
|
726
|
-
return await
|
|
1076
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
727
1077
|
} catch (error) {
|
|
728
1078
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
729
1079
|
throw error;
|
|
730
1080
|
attempt += 1;
|
|
731
1081
|
const delayMs = retryDelay(options, attempt, error);
|
|
1082
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1083
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
1084
|
+
throw new SynthesisTimeoutError(
|
|
1085
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
732
1088
|
onRetry(attempt, delayMs);
|
|
733
|
-
await waitForRetry(delayMs, config.signal);
|
|
1089
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
734
1090
|
}
|
|
735
1091
|
}
|
|
736
1092
|
}
|
|
1093
|
+
async function synthesizeSsml(ssml, config) {
|
|
1094
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1095
|
+
try {
|
|
1096
|
+
deadline.throwIfExpired();
|
|
1097
|
+
const synthesisConfig = {
|
|
1098
|
+
...config,
|
|
1099
|
+
signal: deadline.signal,
|
|
1100
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1101
|
+
};
|
|
1102
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1103
|
+
deadline.throwIfExpired();
|
|
1104
|
+
return result;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1107
|
+
throw error;
|
|
1108
|
+
} finally {
|
|
1109
|
+
deadline.dispose();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
737
1112
|
function createAbortScope(parent, timeoutMs) {
|
|
738
1113
|
const controller = new AbortController();
|
|
739
1114
|
let didTimeout = false;
|
|
@@ -754,10 +1129,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
754
1129
|
abort: () => controller.abort()
|
|
755
1130
|
};
|
|
756
1131
|
}
|
|
757
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
1132
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
758
1133
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
759
1134
|
try {
|
|
760
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
1135
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
761
1136
|
} catch (error) {
|
|
762
1137
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
763
1138
|
throw error;
|
|
@@ -766,18 +1141,42 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
766
1141
|
}
|
|
767
1142
|
}
|
|
768
1143
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
769
|
-
const results = new Array(chunks.length);
|
|
770
1144
|
const totalChunks = chunks.length;
|
|
1145
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1146
|
+
const fingerprints = inputs.map(
|
|
1147
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1148
|
+
region: config.region,
|
|
1149
|
+
endpoint: config.endpoint,
|
|
1150
|
+
customHeaders: config.customHeaders,
|
|
1151
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1152
|
+
})
|
|
1153
|
+
);
|
|
1154
|
+
const results = new Array(totalChunks);
|
|
771
1155
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1156
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1157
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1158
|
+
chunkIndex,
|
|
1159
|
+
status: "pending",
|
|
1160
|
+
canResume: true
|
|
1161
|
+
}));
|
|
772
1162
|
for (const [index, cached] of cachedChunks) {
|
|
773
|
-
if (index
|
|
1163
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
1164
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
1165
|
+
if (isValid) {
|
|
1166
|
+
results[index] = { ...cached };
|
|
1167
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1168
|
+
} else {
|
|
1169
|
+
invalidCachedIndices.add(index);
|
|
1170
|
+
}
|
|
774
1171
|
}
|
|
775
1172
|
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
776
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1173
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1174
|
+
const jobStartedAt = Date.now();
|
|
1175
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1176
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
777
1177
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
778
1178
|
const report = (event) => config.onProgress?.(event);
|
|
779
|
-
for (const [index,
|
|
780
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1179
|
+
for (const [index, input] of inputs.entries()) {
|
|
781
1180
|
report({
|
|
782
1181
|
currentChunk: index,
|
|
783
1182
|
totalChunks,
|
|
@@ -799,8 +1198,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
799
1198
|
if (index >= chunks.length) return;
|
|
800
1199
|
if (!shouldSynthesize(index)) continue;
|
|
801
1200
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
802
|
-
const
|
|
803
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1201
|
+
const input = inputs[index];
|
|
804
1202
|
report({
|
|
805
1203
|
currentChunk: completed,
|
|
806
1204
|
totalChunks,
|
|
@@ -816,7 +1214,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
816
1214
|
input.ssml,
|
|
817
1215
|
{
|
|
818
1216
|
...config,
|
|
819
|
-
signal:
|
|
1217
|
+
signal: deadline.signal,
|
|
820
1218
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
821
1219
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
822
1220
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -837,9 +1235,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
837
1235
|
retryAttempt,
|
|
838
1236
|
nextRetryDelayMs,
|
|
839
1237
|
isRetrying: true
|
|
840
|
-
})
|
|
1238
|
+
}),
|
|
1239
|
+
jobDeadlineAt
|
|
841
1240
|
);
|
|
842
1241
|
results[index] = result;
|
|
1242
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
843
1243
|
completed += 1;
|
|
844
1244
|
report({
|
|
845
1245
|
currentChunk: completed,
|
|
@@ -851,7 +1251,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
851
1251
|
durationMs: Date.now() - startedAt
|
|
852
1252
|
});
|
|
853
1253
|
} catch (error) {
|
|
854
|
-
|
|
1254
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
1255
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
1256
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1257
|
+
chunkStates[index] = {
|
|
1258
|
+
chunkIndex: index,
|
|
1259
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1260
|
+
isOriginalFailure: !wasCancelled,
|
|
1261
|
+
canResume: true,
|
|
1262
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1263
|
+
};
|
|
855
1264
|
report({
|
|
856
1265
|
currentChunk: completed,
|
|
857
1266
|
totalChunks,
|
|
@@ -862,7 +1271,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
862
1271
|
durationMs: Date.now() - startedAt,
|
|
863
1272
|
error
|
|
864
1273
|
});
|
|
865
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
866
1274
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
867
1275
|
return;
|
|
868
1276
|
}
|
|
@@ -871,26 +1279,47 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
871
1279
|
try {
|
|
872
1280
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
873
1281
|
if (firstError) throw firstError;
|
|
1282
|
+
const missingChunkIndices = Array.from(
|
|
1283
|
+
{ length: totalChunks },
|
|
1284
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1285
|
+
).filter((index) => index !== void 0);
|
|
1286
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
874
1287
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
875
1288
|
return await mergeSynthesisResults(orderedResults, {
|
|
876
1289
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
877
|
-
signal:
|
|
1290
|
+
signal: deadline.signal,
|
|
878
1291
|
customMerger: config.customMerger,
|
|
879
1292
|
outputMimeType: config.outputMimeType,
|
|
880
|
-
postMergeValidator: config.postMergeValidator
|
|
1293
|
+
postMergeValidator: config.postMergeValidator,
|
|
1294
|
+
deadline
|
|
881
1295
|
});
|
|
882
1296
|
} catch (error) {
|
|
1297
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
1298
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1299
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1300
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const synthesizedChunks = results.flatMap(
|
|
1305
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1306
|
+
);
|
|
883
1307
|
const partial = {
|
|
884
|
-
synthesizedChunks
|
|
885
|
-
completedChunks:
|
|
886
|
-
pendingChunkIndices:
|
|
1308
|
+
synthesizedChunks,
|
|
1309
|
+
completedChunks: synthesizedChunks,
|
|
1310
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1311
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1312
|
+
),
|
|
887
1313
|
failedChunkIndices: [...failedIndices],
|
|
1314
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1315
|
+
chunkStates,
|
|
888
1316
|
totalChunks
|
|
889
1317
|
};
|
|
890
1318
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
891
1319
|
throw error;
|
|
892
1320
|
} finally {
|
|
893
1321
|
scope.dispose();
|
|
1322
|
+
deadline.dispose();
|
|
894
1323
|
}
|
|
895
1324
|
}
|
|
896
1325
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -968,30 +1397,44 @@ function mergeSynthesisResults(results, options) {
|
|
|
968
1397
|
const format = resolvedOptions?.format;
|
|
969
1398
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
970
1399
|
const buffers = results.map((result) => result.audioData);
|
|
971
|
-
const inputSpecs = results.map((result) =>
|
|
1400
|
+
const inputSpecs = results.map((result) => {
|
|
1401
|
+
if (result.audioSpec) return result.audioSpec;
|
|
1402
|
+
try {
|
|
1403
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1406
|
+
throw error;
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
972
1409
|
validateAudioSpecifications(inputSpecs);
|
|
973
|
-
const
|
|
1410
|
+
const deadline = resolvedOptions.deadline;
|
|
1411
|
+
deadline?.throwIfExpired();
|
|
1412
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
974
1413
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
975
1414
|
if (resolvedOptions.customMerger) {
|
|
976
|
-
return
|
|
977
|
-
()
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1415
|
+
return withinDeadline(
|
|
1416
|
+
Promise.resolve().then(
|
|
1417
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1418
|
+
format,
|
|
1419
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1420
|
+
inputSpecs,
|
|
1421
|
+
signal
|
|
1422
|
+
})
|
|
1423
|
+
),
|
|
1424
|
+
deadline
|
|
983
1425
|
).then((merged) => {
|
|
1426
|
+
deadline?.throwIfExpired();
|
|
984
1427
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
985
|
-
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
986
|
-
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
987
1428
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
988
|
-
const
|
|
989
|
-
results,
|
|
1429
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
990
1430
|
merged,
|
|
991
1431
|
format,
|
|
992
|
-
|
|
993
|
-
|
|
1432
|
+
buffers,
|
|
1433
|
+
inputSpecs,
|
|
1434
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1435
|
+
true
|
|
994
1436
|
);
|
|
1437
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
995
1438
|
return Promise.resolve(
|
|
996
1439
|
resolvedOptions.postMergeValidator?.(result, {
|
|
997
1440
|
format,
|
|
@@ -1000,6 +1443,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1000
1443
|
signal
|
|
1001
1444
|
})
|
|
1002
1445
|
).then((valid) => {
|
|
1446
|
+
deadline?.throwIfExpired();
|
|
1003
1447
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1004
1448
|
return result;
|
|
1005
1449
|
});
|
|
@@ -1010,6 +1454,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1010
1454
|
});
|
|
1011
1455
|
}
|
|
1012
1456
|
try {
|
|
1457
|
+
deadline?.throwIfExpired();
|
|
1013
1458
|
const result = createMergedResult(
|
|
1014
1459
|
results,
|
|
1015
1460
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -1025,12 +1470,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
1025
1470
|
signal
|
|
1026
1471
|
});
|
|
1027
1472
|
if (validation instanceof Promise)
|
|
1028
|
-
return validation.then((valid) => {
|
|
1473
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
1474
|
+
deadline?.throwIfExpired();
|
|
1029
1475
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1030
1476
|
return result;
|
|
1031
1477
|
});
|
|
1032
1478
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1033
1479
|
}
|
|
1480
|
+
deadline?.throwIfExpired();
|
|
1034
1481
|
return result;
|
|
1035
1482
|
} catch (error) {
|
|
1036
1483
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -1120,7 +1567,7 @@ function resolveConcurrency2(value, total) {
|
|
|
1120
1567
|
if (value === Infinity) return Math.max(1, total);
|
|
1121
1568
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1122
1569
|
}
|
|
1123
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1570
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1124
1571
|
const retry = options ? {
|
|
1125
1572
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1126
1573
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -1137,6 +1584,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
1137
1584
|
throw error;
|
|
1138
1585
|
attempt += 1;
|
|
1139
1586
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1587
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1588
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1589
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1590
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1591
|
+
}
|
|
1140
1592
|
onRetry(attempt, delayMs);
|
|
1141
1593
|
if (delayMs > 0)
|
|
1142
1594
|
await new Promise((resolve, reject) => {
|
|
@@ -1170,14 +1622,19 @@ function sharedValidationOptions(options, signal) {
|
|
|
1170
1622
|
};
|
|
1171
1623
|
}
|
|
1172
1624
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1173
|
-
const
|
|
1625
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1626
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1174
1627
|
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
1175
|
-
if (
|
|
1176
|
-
const error = toSynthesisError(
|
|
1628
|
+
if (deadline.signal.aborted) {
|
|
1629
|
+
const error = toSynthesisError(
|
|
1630
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1631
|
+
);
|
|
1632
|
+
deadline.dispose();
|
|
1177
1633
|
return failure(error);
|
|
1178
1634
|
}
|
|
1179
1635
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1180
1636
|
if (errors.length > 0) {
|
|
1637
|
+
deadline.dispose();
|
|
1181
1638
|
return failure({
|
|
1182
1639
|
kind: "validation-error",
|
|
1183
1640
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
@@ -1190,23 +1647,30 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
1190
1647
|
success: true,
|
|
1191
1648
|
status: "success",
|
|
1192
1649
|
value: await client.synthesizeSsml(ssml, {
|
|
1193
|
-
signal:
|
|
1650
|
+
signal: deadline.signal,
|
|
1194
1651
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1195
|
-
timeouts: options.timeouts
|
|
1652
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1196
1653
|
})
|
|
1197
1654
|
};
|
|
1198
1655
|
} catch (error) {
|
|
1656
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1199
1657
|
const synthesisError = toSynthesisError(error);
|
|
1200
1658
|
return failure(synthesisError);
|
|
1659
|
+
} finally {
|
|
1660
|
+
deadline.dispose();
|
|
1201
1661
|
}
|
|
1202
1662
|
}
|
|
1203
1663
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1664
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1204
1665
|
const validationOptions = sharedValidationOptions(
|
|
1205
1666
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1206
|
-
|
|
1667
|
+
deadline.signal
|
|
1207
1668
|
);
|
|
1208
|
-
if (
|
|
1209
|
-
const error = toSynthesisError(
|
|
1669
|
+
if (deadline.signal.aborted) {
|
|
1670
|
+
const error = toSynthesisError(
|
|
1671
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1672
|
+
);
|
|
1673
|
+
deadline.dispose();
|
|
1210
1674
|
return failure(error);
|
|
1211
1675
|
}
|
|
1212
1676
|
const pending = (index, status, error) => {
|
|
@@ -1238,14 +1702,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1238
1702
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1239
1703
|
})
|
|
1240
1704
|
);
|
|
1241
|
-
if (
|
|
1705
|
+
if (deadline.signal.aborted) {
|
|
1242
1706
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1707
|
+
deadline.dispose();
|
|
1243
1708
|
return failure(error);
|
|
1244
1709
|
}
|
|
1245
1710
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1246
1711
|
if (chunkDiagnostics.length > 0) {
|
|
1247
1712
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1248
1713
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1714
|
+
deadline.dispose();
|
|
1249
1715
|
return failure(error);
|
|
1250
1716
|
}
|
|
1251
1717
|
let fallbackJobScope;
|
|
@@ -1258,9 +1724,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1258
1724
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1259
1725
|
onProgress: options.onProgress,
|
|
1260
1726
|
outputFormat: options.outputFormat,
|
|
1261
|
-
signal:
|
|
1727
|
+
signal: deadline.signal,
|
|
1262
1728
|
timeoutMs: options.timeoutMs,
|
|
1263
|
-
timeouts: options.timeouts,
|
|
1729
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1264
1730
|
sourceNodePath: options.sourceNodePath,
|
|
1265
1731
|
concurrency: options.concurrency,
|
|
1266
1732
|
retryOptions: options.retryOptions,
|
|
@@ -1269,18 +1735,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1269
1735
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1270
1736
|
customMerger: options.customMerger,
|
|
1271
1737
|
outputMimeType: options.outputMimeType,
|
|
1272
|
-
postMergeValidator: options.postMergeValidator
|
|
1738
|
+
postMergeValidator: options.postMergeValidator,
|
|
1739
|
+
resumeValidation: options.resumeValidation,
|
|
1740
|
+
customHeaders: options.customHeaders,
|
|
1741
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1273
1742
|
});
|
|
1274
1743
|
return { ok: true, success: true, status: "success", value };
|
|
1275
1744
|
}
|
|
1745
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1746
|
+
const fingerprints = inputs.map(
|
|
1747
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1748
|
+
customHeaders: options.customHeaders,
|
|
1749
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1750
|
+
})
|
|
1751
|
+
);
|
|
1276
1752
|
const results = new Array(chunks.length);
|
|
1753
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1754
|
+
chunkIndex,
|
|
1755
|
+
status: "pending",
|
|
1756
|
+
canResume: true
|
|
1757
|
+
}));
|
|
1277
1758
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1759
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1278
1760
|
for (const [index, cached] of cachedChunks) {
|
|
1279
|
-
if (index
|
|
1761
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
1762
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1763
|
+
results[index] = cached;
|
|
1764
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1765
|
+
} else invalidCachedIndices.add(index);
|
|
1280
1766
|
}
|
|
1281
1767
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1282
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1283
|
-
const
|
|
1768
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1769
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1770
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1284
1771
|
fallbackJobScope = jobScope;
|
|
1285
1772
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
1286
1773
|
let firstError;
|
|
@@ -1292,7 +1779,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1292
1779
|
const index = nextIndex++;
|
|
1293
1780
|
if (index >= chunks.length) return;
|
|
1294
1781
|
if (!shouldSynthesize(index)) continue;
|
|
1295
|
-
if (
|
|
1782
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1783
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1296
1786
|
const chunk = chunks[index];
|
|
1297
1787
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1298
1788
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -1301,8 +1791,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1301
1791
|
const startedAt = Date.now();
|
|
1302
1792
|
try {
|
|
1303
1793
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1304
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
1305
|
-
const chunkSignal = chunkScope?.signal ??
|
|
1794
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1795
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1306
1796
|
let result;
|
|
1307
1797
|
try {
|
|
1308
1798
|
result = await retryableSynthesis(
|
|
@@ -1325,10 +1815,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1325
1815
|
retryAttempt,
|
|
1326
1816
|
nextRetryDelayMs,
|
|
1327
1817
|
isRetrying: true
|
|
1328
|
-
})
|
|
1818
|
+
}),
|
|
1819
|
+
jobDeadlineAt
|
|
1329
1820
|
);
|
|
1330
1821
|
} catch (error) {
|
|
1331
|
-
if (chunkScope?.timedOut())
|
|
1822
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1332
1823
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1333
1824
|
throw error;
|
|
1334
1825
|
} finally {
|
|
@@ -1377,6 +1868,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1377
1868
|
}))
|
|
1378
1869
|
} : {}
|
|
1379
1870
|
};
|
|
1871
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1380
1872
|
completed += 1;
|
|
1381
1873
|
options.onProgress?.({
|
|
1382
1874
|
currentChunk: completed,
|
|
@@ -1388,7 +1880,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1388
1880
|
durationMs: Date.now() - startedAt
|
|
1389
1881
|
});
|
|
1390
1882
|
} catch (error) {
|
|
1391
|
-
|
|
1883
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1884
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1885
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1886
|
+
chunkStates[index] = {
|
|
1887
|
+
chunkIndex: index,
|
|
1888
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1889
|
+
isOriginalFailure: !wasCancelled,
|
|
1890
|
+
canResume: true,
|
|
1891
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1892
|
+
};
|
|
1392
1893
|
options.onProgress?.({
|
|
1393
1894
|
currentChunk: completed,
|
|
1394
1895
|
totalChunks: chunks.length,
|
|
@@ -1400,23 +1901,41 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1400
1901
|
error
|
|
1401
1902
|
});
|
|
1402
1903
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1403
|
-
firstError ?? (firstError = error);
|
|
1404
1904
|
return;
|
|
1405
1905
|
}
|
|
1406
1906
|
}
|
|
1407
1907
|
};
|
|
1408
1908
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1909
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1910
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1911
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1912
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1409
1916
|
if (failedIndices.size > 0) {
|
|
1410
1917
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1918
|
+
const synthesizedChunks = results.flatMap(
|
|
1919
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1920
|
+
);
|
|
1411
1921
|
error.partialResult = {
|
|
1412
|
-
synthesizedChunks
|
|
1413
|
-
completedChunks:
|
|
1414
|
-
pendingChunkIndices:
|
|
1922
|
+
synthesizedChunks,
|
|
1923
|
+
completedChunks: synthesizedChunks,
|
|
1924
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1925
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1926
|
+
),
|
|
1415
1927
|
failedChunkIndices: [...failedIndices],
|
|
1928
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1929
|
+
chunkStates,
|
|
1416
1930
|
totalChunks: chunks.length
|
|
1417
1931
|
};
|
|
1418
1932
|
throw error;
|
|
1419
1933
|
}
|
|
1934
|
+
const missingChunkIndices = Array.from(
|
|
1935
|
+
{ length: chunks.length },
|
|
1936
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1937
|
+
).filter((index) => index !== void 0);
|
|
1938
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1420
1939
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1421
1940
|
return {
|
|
1422
1941
|
ok: true,
|
|
@@ -1424,7 +1943,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1424
1943
|
status: "success",
|
|
1425
1944
|
value: await mergeSynthesisResults(orderedResults, {
|
|
1426
1945
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1427
|
-
signal: jobScope?.signal ??
|
|
1946
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
1428
1947
|
customMerger: options.customMerger,
|
|
1429
1948
|
outputMimeType: options.outputMimeType,
|
|
1430
1949
|
postMergeValidator: options.postMergeValidator
|
|
@@ -1435,6 +1954,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1435
1954
|
return failure(synthesisError, partialResultFrom(error));
|
|
1436
1955
|
} finally {
|
|
1437
1956
|
fallbackJobScope?.dispose();
|
|
1957
|
+
deadline.dispose();
|
|
1438
1958
|
}
|
|
1439
1959
|
}
|
|
1440
1960
|
function withValidationSignal(options, signal) {
|
|
@@ -1455,14 +1975,43 @@ var AzureTtsClient = class {
|
|
|
1455
1975
|
__privateSet(this, _options, options);
|
|
1456
1976
|
}
|
|
1457
1977
|
async synthesize(ssml) {
|
|
1458
|
-
const {
|
|
1978
|
+
const {
|
|
1979
|
+
region,
|
|
1980
|
+
subscriptionKey,
|
|
1981
|
+
outputFormat,
|
|
1982
|
+
signal,
|
|
1983
|
+
timeoutMs,
|
|
1984
|
+
timeouts,
|
|
1985
|
+
customHeaders,
|
|
1986
|
+
fingerprintSchemaVersion
|
|
1987
|
+
} = __privateGet(this, _options);
|
|
1459
1988
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1460
1989
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1461
|
-
const config = {
|
|
1990
|
+
const config = {
|
|
1991
|
+
endpoint,
|
|
1992
|
+
region,
|
|
1993
|
+
subscriptionKey,
|
|
1994
|
+
outputFormat,
|
|
1995
|
+
signal,
|
|
1996
|
+
timeoutMs,
|
|
1997
|
+
timeouts,
|
|
1998
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
1999
|
+
customHeaders,
|
|
2000
|
+
fingerprintSchemaVersion
|
|
2001
|
+
};
|
|
1462
2002
|
return synthesizeSpeech(ssml, config);
|
|
1463
2003
|
}
|
|
1464
2004
|
async synthesizeSsml(ssml, options = {}) {
|
|
1465
|
-
const {
|
|
2005
|
+
const {
|
|
2006
|
+
region,
|
|
2007
|
+
subscriptionKey,
|
|
2008
|
+
outputFormat,
|
|
2009
|
+
signal,
|
|
2010
|
+
timeoutMs,
|
|
2011
|
+
timeouts,
|
|
2012
|
+
customHeaders,
|
|
2013
|
+
fingerprintSchemaVersion
|
|
2014
|
+
} = __privateGet(this, _options);
|
|
1466
2015
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1467
2016
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1468
2017
|
return synthesizeSsml(ssml, {
|
|
@@ -1475,11 +2024,28 @@ var AzureTtsClient = class {
|
|
|
1475
2024
|
timeouts: options.timeouts ?? timeouts,
|
|
1476
2025
|
sourceNodePath: options.sourceNodePath,
|
|
1477
2026
|
sourceTextSegments: options.sourceTextSegments,
|
|
1478
|
-
sourceMarkers: options.sourceMarkers
|
|
2027
|
+
sourceMarkers: options.sourceMarkers,
|
|
2028
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2029
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2030
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2031
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2032
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2033
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2034
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2035
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1479
2036
|
});
|
|
1480
2037
|
}
|
|
1481
2038
|
async synthesizeChunks(chunks, options = {}) {
|
|
1482
|
-
const {
|
|
2039
|
+
const {
|
|
2040
|
+
region,
|
|
2041
|
+
subscriptionKey,
|
|
2042
|
+
outputFormat,
|
|
2043
|
+
signal,
|
|
2044
|
+
timeoutMs,
|
|
2045
|
+
timeouts,
|
|
2046
|
+
customHeaders,
|
|
2047
|
+
fingerprintSchemaVersion
|
|
2048
|
+
} = __privateGet(this, _options);
|
|
1483
2049
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1484
2050
|
return synthesizeSsmlChunks(chunks, {
|
|
1485
2051
|
endpoint,
|
|
@@ -1493,12 +2059,15 @@ var AzureTtsClient = class {
|
|
|
1493
2059
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1494
2060
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1495
2061
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1496
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
2062
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1497
2063
|
resumeChunks: options.resumeChunks,
|
|
1498
2064
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1499
|
-
customMerger: options.customMerger,
|
|
1500
|
-
outputMimeType: options.outputMimeType,
|
|
1501
|
-
postMergeValidator: options.postMergeValidator
|
|
2065
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2066
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2067
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2068
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2069
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2070
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1502
2071
|
});
|
|
1503
2072
|
}
|
|
1504
2073
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1513,7 +2082,14 @@ var AzureTtsClient = class {
|
|
|
1513
2082
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1514
2083
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1515
2084
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1516
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
2085
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2086
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2087
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2088
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2089
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2090
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2091
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2092
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
1517
2093
|
});
|
|
1518
2094
|
}
|
|
1519
2095
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1618,6 +2194,8 @@ export {
|
|
|
1618
2194
|
BatchChunkValidationError,
|
|
1619
2195
|
ChunkValidationError,
|
|
1620
2196
|
DEFAULT_OUTPUT_FORMAT,
|
|
2197
|
+
DeadlineController,
|
|
2198
|
+
IncompleteChunkSetError,
|
|
1621
2199
|
MergeError,
|
|
1622
2200
|
SynthesisCancelledError,
|
|
1623
2201
|
SynthesisTimeoutError,
|
|
@@ -1626,6 +2204,7 @@ export {
|
|
|
1626
2204
|
buildPartialSsml,
|
|
1627
2205
|
buildSsml,
|
|
1628
2206
|
canMergeAudioFormat,
|
|
2207
|
+
computeChunkFingerprint,
|
|
1629
2208
|
createAzureUrlValidatorRunner,
|
|
1630
2209
|
extractSsmlText,
|
|
1631
2210
|
extractSsmlTranslatableText,
|
|
@@ -1644,6 +2223,7 @@ export {
|
|
|
1644
2223
|
parseSsml,
|
|
1645
2224
|
resolveMergeAudioFormat,
|
|
1646
2225
|
resolveMimeType,
|
|
2226
|
+
serializeChunkError,
|
|
1647
2227
|
splitSsmlDocument,
|
|
1648
2228
|
synthesizeSpeech,
|
|
1649
2229
|
synthesizeSsml,
|