@ssml-builder-js/azure-tts-client 2.18.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/dist/index.js +453 -63
- package/package.json +8 -8
- package/src/client.ts +43 -3
- package/src/deadline.ts +52 -0
- package/src/errors.ts +86 -4
- package/src/index.ts +11 -1
- package/src/safe.ts +52 -31
- package/src/synthesis.ts +307 -22
- package/src/types.ts +8 -3
- package/test/synthesis.test.ts +20 -0
- package/test/v219-pipeline.test.ts +265 -0
- package/dist/index.d.mts +0 -487
- package/dist/index.d.ts +0 -487
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1813
- package/dist/index.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ __export(index_exports, {
|
|
|
44
44
|
BatchChunkValidationError: () => BatchChunkValidationError,
|
|
45
45
|
ChunkValidationError: () => ChunkValidationError,
|
|
46
46
|
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
47
|
+
DeadlineController: () => DeadlineController,
|
|
48
|
+
IncompleteChunkSetError: () => IncompleteChunkSetError,
|
|
47
49
|
MergeError: () => MergeError,
|
|
48
50
|
SynthesisCancelledError: () => SynthesisCancelledError,
|
|
49
51
|
SynthesisTimeoutError: () => SynthesisTimeoutError,
|
|
@@ -57,6 +59,7 @@ __export(index_exports, {
|
|
|
57
59
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
58
60
|
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
59
61
|
resolveMimeType: () => resolveMimeType,
|
|
62
|
+
serializeChunkError: () => serializeChunkError,
|
|
60
63
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
61
64
|
synthesizeSsml: () => synthesizeSsml,
|
|
62
65
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
@@ -128,6 +131,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
128
131
|
this.name = "SynthesisTimeoutError";
|
|
129
132
|
}
|
|
130
133
|
};
|
|
134
|
+
var IncompleteChunkSetError = class extends Error {
|
|
135
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
136
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
137
|
+
this.kind = "incomplete-chunk-set";
|
|
138
|
+
this.name = "IncompleteChunkSetError";
|
|
139
|
+
this.totalChunks = totalChunks;
|
|
140
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
141
|
+
}
|
|
142
|
+
};
|
|
131
143
|
var MergeError = class extends Error {
|
|
132
144
|
constructor(message, cause) {
|
|
133
145
|
super(message);
|
|
@@ -152,8 +164,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
152
164
|
this.format = format;
|
|
153
165
|
}
|
|
154
166
|
};
|
|
167
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
170
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
171
|
+
const code = kind === "validation-error" ? "VALIDATION_ERROR" : kind === "timeout" || /tim(?:e|ed) ?out|deadline/i.test(message) ? "TIMEOUT" : kind === "cancelled" || /cancel|abort/i.test(message) ? "CANCELLED" : kind === "audio-format-mismatch" || kind === "unsupported-format-error" ? "FORMAT_MISMATCH" : kind === "merge-error" || phase === "merge" ? "MERGE_ERROR" : "AZURE_API_ERROR";
|
|
172
|
+
const details = {};
|
|
173
|
+
if (error instanceof AzureTtsError) {
|
|
174
|
+
details.statusText = error.statusText;
|
|
175
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
176
|
+
}
|
|
177
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
178
|
+
details.totalChunks = error.totalChunks;
|
|
179
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
code,
|
|
183
|
+
phase,
|
|
184
|
+
message,
|
|
185
|
+
isOriginalFailure,
|
|
186
|
+
isRetryable: code === "AZURE_API_ERROR" && (status === 429 || status !== void 0 && status >= 500 || /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message)),
|
|
187
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
188
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
155
191
|
function toSynthesisError(error) {
|
|
156
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
192
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
157
193
|
return error;
|
|
158
194
|
const message = error instanceof Error ? error.message : String(error);
|
|
159
195
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -165,6 +201,55 @@ function createSpeechSdkError(error) {
|
|
|
165
201
|
return new AzureTtsSdkError(message);
|
|
166
202
|
}
|
|
167
203
|
|
|
204
|
+
// src/deadline.ts
|
|
205
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
206
|
+
var DeadlineController = class {
|
|
207
|
+
constructor(totalJobMs, parent) {
|
|
208
|
+
__privateAdd(this, _controller, new AbortController());
|
|
209
|
+
__privateAdd(this, _parent);
|
|
210
|
+
__privateAdd(this, _onParentAbort);
|
|
211
|
+
__privateAdd(this, _timer);
|
|
212
|
+
__privateAdd(this, _timedOut, false);
|
|
213
|
+
__privateSet(this, _parent, parent);
|
|
214
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
215
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
216
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
217
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
218
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
219
|
+
if (this.deadlineAtMs !== void 0) {
|
|
220
|
+
__privateSet(this, _timer, setTimeout(
|
|
221
|
+
() => {
|
|
222
|
+
__privateSet(this, _timedOut, true);
|
|
223
|
+
__privateGet(this, _controller).abort();
|
|
224
|
+
},
|
|
225
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
226
|
+
));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
get timedOut() {
|
|
230
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
231
|
+
}
|
|
232
|
+
get remainingMs() {
|
|
233
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
234
|
+
}
|
|
235
|
+
throwIfExpired() {
|
|
236
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
237
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
238
|
+
}
|
|
239
|
+
abort() {
|
|
240
|
+
__privateGet(this, _controller).abort();
|
|
241
|
+
}
|
|
242
|
+
dispose() {
|
|
243
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
244
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
_controller = new WeakMap();
|
|
248
|
+
_parent = new WeakMap();
|
|
249
|
+
_onParentAbort = new WeakMap();
|
|
250
|
+
_timer = new WeakMap();
|
|
251
|
+
_timedOut = new WeakMap();
|
|
252
|
+
|
|
168
253
|
// src/synthesis.ts
|
|
169
254
|
var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
170
255
|
var import_ssml_core = require("@ssml-builder-js/ssml-core");
|
|
@@ -247,18 +332,23 @@ function createSpeechConfig(config) {
|
|
|
247
332
|
}
|
|
248
333
|
|
|
249
334
|
// src/synthesis.ts
|
|
250
|
-
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT) {
|
|
335
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
251
336
|
const readAttribute = (name) => {
|
|
252
337
|
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
253
338
|
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
254
339
|
};
|
|
340
|
+
const headers = Object.fromEntries(
|
|
341
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
342
|
+
);
|
|
255
343
|
const payload = JSON.stringify({
|
|
256
344
|
ssml,
|
|
257
345
|
outputFormat,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
346
|
+
region: options.region ?? "",
|
|
347
|
+
endpoint: options.endpoint ?? "",
|
|
348
|
+
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
349
|
+
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
350
|
+
customHeaders: headers,
|
|
351
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
262
352
|
});
|
|
263
353
|
let hash = 0xcbf29ce484222325n;
|
|
264
354
|
const mask = 0xffffffffffffffffn;
|
|
@@ -378,6 +468,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
378
468
|
}
|
|
379
469
|
return void 0;
|
|
380
470
|
}
|
|
471
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
472
|
+
const first = bytes[offset];
|
|
473
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
474
|
+
let mask = 128;
|
|
475
|
+
let length = 1;
|
|
476
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
477
|
+
mask >>= 1;
|
|
478
|
+
length += 1;
|
|
479
|
+
}
|
|
480
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
481
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
482
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
483
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
484
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
485
|
+
return { value, length };
|
|
486
|
+
}
|
|
487
|
+
function readEbmlElement(bytes, offset) {
|
|
488
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
489
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
490
|
+
const dataStart = offset + id.length + size.length;
|
|
491
|
+
const dataEnd = dataStart + size.value;
|
|
492
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
493
|
+
return { id: id.value, dataStart, dataEnd };
|
|
494
|
+
}
|
|
495
|
+
function ebmlText(bytes, element) {
|
|
496
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
497
|
+
}
|
|
498
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
499
|
+
let offset = start;
|
|
500
|
+
while (offset < end) {
|
|
501
|
+
const element = readEbmlElement(bytes, offset);
|
|
502
|
+
if (element.id === id) return element;
|
|
503
|
+
offset = element.dataEnd;
|
|
504
|
+
}
|
|
505
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
506
|
+
return void 0;
|
|
507
|
+
}
|
|
508
|
+
function parseOggSpecification(buffer, format) {
|
|
509
|
+
const bytes = new Uint8Array(buffer);
|
|
510
|
+
let offset = 0;
|
|
511
|
+
let firstPayload;
|
|
512
|
+
let pages = 0;
|
|
513
|
+
while (offset < bytes.byteLength) {
|
|
514
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
515
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
516
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
517
|
+
const lacingStart = offset + 27;
|
|
518
|
+
const payloadStart = lacingStart + segmentCount;
|
|
519
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
520
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
521
|
+
const pageEnd = payloadStart + payloadLength;
|
|
522
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
523
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
524
|
+
offset = pageEnd;
|
|
525
|
+
pages += 1;
|
|
526
|
+
}
|
|
527
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
528
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
529
|
+
const version = firstPayload[8];
|
|
530
|
+
const channels = firstPayload[9] ?? 0;
|
|
531
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
532
|
+
12,
|
|
533
|
+
true
|
|
534
|
+
);
|
|
535
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
536
|
+
return {
|
|
537
|
+
format,
|
|
538
|
+
mimeType: "audio/ogg",
|
|
539
|
+
codec: "opus",
|
|
540
|
+
sampleRate,
|
|
541
|
+
channels,
|
|
542
|
+
container: "ogg",
|
|
543
|
+
isVbr: true,
|
|
544
|
+
isCompressed: true
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function parseWebmSpecification(buffer, format) {
|
|
548
|
+
const bytes = new Uint8Array(buffer);
|
|
549
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
550
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
551
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
552
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
553
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
554
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
555
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
556
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
557
|
+
let offset = tracks.dataStart;
|
|
558
|
+
let opusTrack;
|
|
559
|
+
while (offset < tracks.dataEnd) {
|
|
560
|
+
const track = readEbmlElement(bytes, offset);
|
|
561
|
+
if (track.id === 174) {
|
|
562
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
563
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
564
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
565
|
+
opusTrack = track;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
offset = track.dataEnd;
|
|
570
|
+
}
|
|
571
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
572
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
573
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
574
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
575
|
+
const sampleRate = sampling ? new DataView(
|
|
576
|
+
bytes.buffer,
|
|
577
|
+
bytes.byteOffset + sampling.dataStart,
|
|
578
|
+
sampling.dataEnd - sampling.dataStart
|
|
579
|
+
).getFloat64(0, false) : 0;
|
|
580
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
581
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
582
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
583
|
+
return {
|
|
584
|
+
format,
|
|
585
|
+
mimeType: "audio/webm",
|
|
586
|
+
codec: "opus",
|
|
587
|
+
sampleRate: Math.round(sampleRate),
|
|
588
|
+
channels: channelCount,
|
|
589
|
+
container: "webm",
|
|
590
|
+
isVbr: true,
|
|
591
|
+
isCompressed: true
|
|
592
|
+
};
|
|
593
|
+
}
|
|
381
594
|
function inspectAudioSpecification(buffer, format) {
|
|
382
595
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
383
596
|
const parsed = parseWav(buffer);
|
|
@@ -402,7 +615,20 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
402
615
|
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
403
616
|
};
|
|
404
617
|
}
|
|
405
|
-
if (isMp3Format(format))
|
|
618
|
+
if (isMp3Format(format)) {
|
|
619
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
620
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
621
|
+
}
|
|
622
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
623
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
624
|
+
validateContainerFormat(specification2, format);
|
|
625
|
+
return specification2;
|
|
626
|
+
}
|
|
627
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
628
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
629
|
+
validateContainerFormat(specification2, format);
|
|
630
|
+
return specification2;
|
|
631
|
+
}
|
|
406
632
|
const specification = formatAudioSpecification(format);
|
|
407
633
|
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
408
634
|
return specification;
|
|
@@ -411,7 +637,20 @@ function validateRawAudioBuffer(buffer, specification) {
|
|
|
411
637
|
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
412
638
|
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
413
639
|
}
|
|
414
|
-
if (specification.codec === "siren"
|
|
640
|
+
if (specification.codec === "siren") return;
|
|
641
|
+
if (specification.codec === "silk") {
|
|
642
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
643
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
647
|
+
if (specification.codec === "opus") {
|
|
648
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
649
|
+
const frameCountCode = packetCode & 3;
|
|
650
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
651
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
415
654
|
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
416
655
|
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
417
656
|
throw new Error(
|
|
@@ -419,6 +658,15 @@ function validateRawAudioBuffer(buffer, specification) {
|
|
|
419
658
|
);
|
|
420
659
|
}
|
|
421
660
|
}
|
|
661
|
+
function validateContainerFormat(specification, format) {
|
|
662
|
+
const expected = formatAudioSpecification(format);
|
|
663
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
664
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
665
|
+
expected,
|
|
666
|
+
specification
|
|
667
|
+
]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
422
670
|
function validateAudioSpecifications(specs) {
|
|
423
671
|
const first = specs[0];
|
|
424
672
|
if (!first) return;
|
|
@@ -500,17 +748,29 @@ function stripMp3Tags(buffer) {
|
|
|
500
748
|
function isMp3Format(format) {
|
|
501
749
|
return /(?:mp3|mpeg)/i.test(format);
|
|
502
750
|
}
|
|
751
|
+
function isOggFormat(format) {
|
|
752
|
+
return /ogg/i.test(format);
|
|
753
|
+
}
|
|
754
|
+
function isWebmFormat(format) {
|
|
755
|
+
return /webm/i.test(format);
|
|
756
|
+
}
|
|
503
757
|
function isWavFormat(format) {
|
|
504
758
|
return /(?:wav|wave|riff)/i.test(format);
|
|
505
759
|
}
|
|
506
760
|
function isRawFormat(format) {
|
|
507
761
|
return /^raw(?:-|$)/i.test(format);
|
|
508
762
|
}
|
|
509
|
-
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType) {
|
|
763
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
510
764
|
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
511
765
|
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
512
766
|
}
|
|
513
|
-
|
|
767
|
+
let specification;
|
|
768
|
+
try {
|
|
769
|
+
specification = inspectAudioSpecification(merged, format);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (!allowExternalContainer) throw error;
|
|
772
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
773
|
+
}
|
|
514
774
|
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
515
775
|
const firstInput = inputSpecs[0];
|
|
516
776
|
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
@@ -530,6 +790,25 @@ function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMi
|
|
|
530
790
|
}
|
|
531
791
|
return specification;
|
|
532
792
|
}
|
|
793
|
+
async function withinDeadline(value, deadline) {
|
|
794
|
+
if (!deadline) return value;
|
|
795
|
+
deadline.throwIfExpired();
|
|
796
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
797
|
+
let timer;
|
|
798
|
+
try {
|
|
799
|
+
return await Promise.race([
|
|
800
|
+
Promise.resolve(value),
|
|
801
|
+
new Promise((_resolve, reject) => {
|
|
802
|
+
timer = setTimeout(
|
|
803
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
804
|
+
deadline.remainingMs
|
|
805
|
+
);
|
|
806
|
+
})
|
|
807
|
+
]);
|
|
808
|
+
} finally {
|
|
809
|
+
if (timer) clearTimeout(timer);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
533
812
|
function resolveMergeAudioFormat(format) {
|
|
534
813
|
if (isWavFormat(format)) return "wav";
|
|
535
814
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -542,6 +821,7 @@ function canMergeAudioFormat(format) {
|
|
|
542
821
|
function mergeAudioBuffers(buffers, options) {
|
|
543
822
|
const format = typeof options === "string" ? options : options?.format;
|
|
544
823
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
824
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
545
825
|
try {
|
|
546
826
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
547
827
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -850,10 +1130,23 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadline
|
|
|
850
1130
|
}
|
|
851
1131
|
}
|
|
852
1132
|
async function synthesizeSsml(ssml, config) {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1133
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1134
|
+
try {
|
|
1135
|
+
deadline.throwIfExpired();
|
|
1136
|
+
const synthesisConfig = {
|
|
1137
|
+
...config,
|
|
1138
|
+
signal: deadline.signal,
|
|
1139
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1140
|
+
};
|
|
1141
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1142
|
+
deadline.throwIfExpired();
|
|
1143
|
+
return result;
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1146
|
+
throw error;
|
|
1147
|
+
} finally {
|
|
1148
|
+
deadline.dispose();
|
|
1149
|
+
}
|
|
857
1150
|
}
|
|
858
1151
|
function createAbortScope(parent, timeoutMs) {
|
|
859
1152
|
const controller = new AbortController();
|
|
@@ -890,7 +1183,12 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
890
1183
|
const totalChunks = chunks.length;
|
|
891
1184
|
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
892
1185
|
const fingerprints = inputs.map(
|
|
893
|
-
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT
|
|
1186
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1187
|
+
region: config.region,
|
|
1188
|
+
endpoint: config.endpoint,
|
|
1189
|
+
customHeaders: config.customHeaders,
|
|
1190
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1191
|
+
})
|
|
894
1192
|
);
|
|
895
1193
|
const results = new Array(totalChunks);
|
|
896
1194
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
@@ -914,6 +1212,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
914
1212
|
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
915
1213
|
const jobStartedAt = Date.now();
|
|
916
1214
|
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1215
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
917
1216
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
918
1217
|
const report = (event) => config.onProgress?.(event);
|
|
919
1218
|
for (const [index, input] of inputs.entries()) {
|
|
@@ -954,7 +1253,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
954
1253
|
input.ssml,
|
|
955
1254
|
{
|
|
956
1255
|
...config,
|
|
957
|
-
signal:
|
|
1256
|
+
signal: deadline.signal,
|
|
958
1257
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
959
1258
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
960
1259
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -999,7 +1298,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
999
1298
|
status: wasCancelled ? "cancelled" : "failed",
|
|
1000
1299
|
isOriginalFailure: !wasCancelled,
|
|
1001
1300
|
canResume: true,
|
|
1002
|
-
error
|
|
1301
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1003
1302
|
};
|
|
1004
1303
|
report({
|
|
1005
1304
|
currentChunk: completed,
|
|
@@ -1019,13 +1318,19 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
1019
1318
|
try {
|
|
1020
1319
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1021
1320
|
if (firstError) throw firstError;
|
|
1321
|
+
const missingChunkIndices = Array.from(
|
|
1322
|
+
{ length: totalChunks },
|
|
1323
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1324
|
+
).filter((index) => index !== void 0);
|
|
1325
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
1022
1326
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1023
1327
|
return await mergeSynthesisResults(orderedResults, {
|
|
1024
1328
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
1025
|
-
signal:
|
|
1329
|
+
signal: deadline.signal,
|
|
1026
1330
|
customMerger: config.customMerger,
|
|
1027
1331
|
outputMimeType: config.outputMimeType,
|
|
1028
|
-
postMergeValidator: config.postMergeValidator
|
|
1332
|
+
postMergeValidator: config.postMergeValidator,
|
|
1333
|
+
deadline
|
|
1029
1334
|
});
|
|
1030
1335
|
} catch (error) {
|
|
1031
1336
|
if (firstError && config.cancelOnFailure !== false) {
|
|
@@ -1053,6 +1358,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
1053
1358
|
throw error;
|
|
1054
1359
|
} finally {
|
|
1055
1360
|
scope.dispose();
|
|
1361
|
+
deadline.dispose();
|
|
1056
1362
|
}
|
|
1057
1363
|
}
|
|
1058
1364
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -1130,19 +1436,33 @@ function mergeSynthesisResults(results, options) {
|
|
|
1130
1436
|
const format = resolvedOptions?.format;
|
|
1131
1437
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
1132
1438
|
const buffers = results.map((result) => result.audioData);
|
|
1133
|
-
const inputSpecs = results.map((result) =>
|
|
1439
|
+
const inputSpecs = results.map((result) => {
|
|
1440
|
+
if (result.audioSpec) return result.audioSpec;
|
|
1441
|
+
try {
|
|
1442
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1134
1448
|
validateAudioSpecifications(inputSpecs);
|
|
1135
|
-
const
|
|
1449
|
+
const deadline = resolvedOptions.deadline;
|
|
1450
|
+
deadline?.throwIfExpired();
|
|
1451
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
1136
1452
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1137
1453
|
if (resolvedOptions.customMerger) {
|
|
1138
|
-
return
|
|
1139
|
-
()
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1454
|
+
return withinDeadline(
|
|
1455
|
+
Promise.resolve().then(
|
|
1456
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1457
|
+
format,
|
|
1458
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1459
|
+
inputSpecs,
|
|
1460
|
+
signal
|
|
1461
|
+
})
|
|
1462
|
+
),
|
|
1463
|
+
deadline
|
|
1145
1464
|
).then((merged) => {
|
|
1465
|
+
deadline?.throwIfExpired();
|
|
1146
1466
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
1147
1467
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1148
1468
|
const mergedSpec = validateMergedAudioBuffer(
|
|
@@ -1150,7 +1470,8 @@ function mergeSynthesisResults(results, options) {
|
|
|
1150
1470
|
format,
|
|
1151
1471
|
buffers,
|
|
1152
1472
|
inputSpecs,
|
|
1153
|
-
resolvedOptions.outputMimeType ?? resolveMimeType(format)
|
|
1473
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1474
|
+
true
|
|
1154
1475
|
);
|
|
1155
1476
|
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
1156
1477
|
return Promise.resolve(
|
|
@@ -1161,6 +1482,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1161
1482
|
signal
|
|
1162
1483
|
})
|
|
1163
1484
|
).then((valid) => {
|
|
1485
|
+
deadline?.throwIfExpired();
|
|
1164
1486
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1165
1487
|
return result;
|
|
1166
1488
|
});
|
|
@@ -1171,6 +1493,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1171
1493
|
});
|
|
1172
1494
|
}
|
|
1173
1495
|
try {
|
|
1496
|
+
deadline?.throwIfExpired();
|
|
1174
1497
|
const result = createMergedResult(
|
|
1175
1498
|
results,
|
|
1176
1499
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -1186,12 +1509,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
1186
1509
|
signal
|
|
1187
1510
|
});
|
|
1188
1511
|
if (validation instanceof Promise)
|
|
1189
|
-
return validation.then((valid) => {
|
|
1512
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
1513
|
+
deadline?.throwIfExpired();
|
|
1190
1514
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1191
1515
|
return result;
|
|
1192
1516
|
});
|
|
1193
1517
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1194
1518
|
}
|
|
1519
|
+
deadline?.throwIfExpired();
|
|
1195
1520
|
return result;
|
|
1196
1521
|
} catch (error) {
|
|
1197
1522
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -1337,47 +1662,55 @@ function sharedValidationOptions(options, signal) {
|
|
|
1337
1662
|
};
|
|
1338
1663
|
}
|
|
1339
1664
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1340
|
-
const
|
|
1665
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1666
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1341
1667
|
const diagnostics = await Promise.resolve((0, import_ssml_core2.validateAzureSsml)(ssml, validationOptions));
|
|
1342
|
-
if (
|
|
1343
|
-
const error = toSynthesisError(
|
|
1668
|
+
if (deadline.signal.aborted) {
|
|
1669
|
+
const error = toSynthesisError(
|
|
1670
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1671
|
+
);
|
|
1672
|
+
deadline.dispose();
|
|
1344
1673
|
return failure(error);
|
|
1345
1674
|
}
|
|
1346
1675
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1347
1676
|
if (errors.length > 0) {
|
|
1677
|
+
deadline.dispose();
|
|
1348
1678
|
return failure({
|
|
1349
1679
|
kind: "validation-error",
|
|
1350
1680
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
1351
1681
|
diagnostics: errors
|
|
1352
1682
|
});
|
|
1353
1683
|
}
|
|
1354
|
-
const jobScope = options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts.totalJobMs) : void 0;
|
|
1355
1684
|
try {
|
|
1356
1685
|
return {
|
|
1357
1686
|
ok: true,
|
|
1358
1687
|
success: true,
|
|
1359
1688
|
status: "success",
|
|
1360
1689
|
value: await client.synthesizeSsml(ssml, {
|
|
1361
|
-
signal:
|
|
1690
|
+
signal: deadline.signal,
|
|
1362
1691
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1363
|
-
timeouts: options.timeouts
|
|
1692
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1364
1693
|
})
|
|
1365
1694
|
};
|
|
1366
1695
|
} catch (error) {
|
|
1367
|
-
if (
|
|
1696
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1368
1697
|
const synthesisError = toSynthesisError(error);
|
|
1369
1698
|
return failure(synthesisError);
|
|
1370
1699
|
} finally {
|
|
1371
|
-
|
|
1700
|
+
deadline.dispose();
|
|
1372
1701
|
}
|
|
1373
1702
|
}
|
|
1374
1703
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1704
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1375
1705
|
const validationOptions = sharedValidationOptions(
|
|
1376
1706
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1377
|
-
|
|
1707
|
+
deadline.signal
|
|
1378
1708
|
);
|
|
1379
|
-
if (
|
|
1380
|
-
const error = toSynthesisError(
|
|
1709
|
+
if (deadline.signal.aborted) {
|
|
1710
|
+
const error = toSynthesisError(
|
|
1711
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1712
|
+
);
|
|
1713
|
+
deadline.dispose();
|
|
1381
1714
|
return failure(error);
|
|
1382
1715
|
}
|
|
1383
1716
|
const pending = (index, status, error) => {
|
|
@@ -1409,14 +1742,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1409
1742
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1410
1743
|
})
|
|
1411
1744
|
);
|
|
1412
|
-
if (
|
|
1745
|
+
if (deadline.signal.aborted) {
|
|
1413
1746
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1747
|
+
deadline.dispose();
|
|
1414
1748
|
return failure(error);
|
|
1415
1749
|
}
|
|
1416
1750
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1417
1751
|
if (chunkDiagnostics.length > 0) {
|
|
1418
1752
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1419
1753
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1754
|
+
deadline.dispose();
|
|
1420
1755
|
return failure(error);
|
|
1421
1756
|
}
|
|
1422
1757
|
let fallbackJobScope;
|
|
@@ -1429,9 +1764,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1429
1764
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1430
1765
|
onProgress: options.onProgress,
|
|
1431
1766
|
outputFormat: options.outputFormat,
|
|
1432
|
-
signal:
|
|
1767
|
+
signal: deadline.signal,
|
|
1433
1768
|
timeoutMs: options.timeoutMs,
|
|
1434
|
-
timeouts: options.timeouts,
|
|
1769
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1435
1770
|
sourceNodePath: options.sourceNodePath,
|
|
1436
1771
|
concurrency: options.concurrency,
|
|
1437
1772
|
retryOptions: options.retryOptions,
|
|
@@ -1441,12 +1776,19 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1441
1776
|
customMerger: options.customMerger,
|
|
1442
1777
|
outputMimeType: options.outputMimeType,
|
|
1443
1778
|
postMergeValidator: options.postMergeValidator,
|
|
1444
|
-
resumeValidation: options.resumeValidation
|
|
1779
|
+
resumeValidation: options.resumeValidation,
|
|
1780
|
+
customHeaders: options.customHeaders,
|
|
1781
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1445
1782
|
});
|
|
1446
1783
|
return { ok: true, success: true, status: "success", value };
|
|
1447
1784
|
}
|
|
1448
1785
|
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1449
|
-
const fingerprints = inputs.map(
|
|
1786
|
+
const fingerprints = inputs.map(
|
|
1787
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1788
|
+
customHeaders: options.customHeaders,
|
|
1789
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1450
1792
|
const results = new Array(chunks.length);
|
|
1451
1793
|
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1452
1794
|
chunkIndex,
|
|
@@ -1464,9 +1806,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1464
1806
|
}
|
|
1465
1807
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1466
1808
|
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1809
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1810
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1470
1811
|
fallbackJobScope = jobScope;
|
|
1471
1812
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
1472
1813
|
let firstError;
|
|
@@ -1490,8 +1831,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1490
1831
|
const startedAt = Date.now();
|
|
1491
1832
|
try {
|
|
1492
1833
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1493
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
1494
|
-
const chunkSignal = chunkScope?.signal ??
|
|
1834
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1835
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1495
1836
|
let result;
|
|
1496
1837
|
try {
|
|
1497
1838
|
result = await retryableSynthesis(
|
|
@@ -1518,7 +1859,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1518
1859
|
jobDeadlineAt
|
|
1519
1860
|
);
|
|
1520
1861
|
} catch (error) {
|
|
1521
|
-
if (chunkScope?.timedOut())
|
|
1862
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1522
1863
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1523
1864
|
throw error;
|
|
1524
1865
|
} finally {
|
|
@@ -1579,15 +1920,15 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1579
1920
|
durationMs: Date.now() - startedAt
|
|
1580
1921
|
});
|
|
1581
1922
|
} catch (error) {
|
|
1582
|
-
const wasCancelled = firstError !== void 0 || Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1583
|
-
firstError ?? (firstError = error);
|
|
1923
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1924
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1584
1925
|
if (!wasCancelled) failedIndices.add(index);
|
|
1585
1926
|
chunkStates[index] = {
|
|
1586
1927
|
chunkIndex: index,
|
|
1587
1928
|
status: wasCancelled ? "cancelled" : "failed",
|
|
1588
1929
|
isOriginalFailure: !wasCancelled,
|
|
1589
1930
|
canResume: true,
|
|
1590
|
-
error
|
|
1931
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1591
1932
|
};
|
|
1592
1933
|
options.onProgress?.({
|
|
1593
1934
|
currentChunk: completed,
|
|
@@ -1630,6 +1971,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1630
1971
|
};
|
|
1631
1972
|
throw error;
|
|
1632
1973
|
}
|
|
1974
|
+
const missingChunkIndices = Array.from(
|
|
1975
|
+
{ length: chunks.length },
|
|
1976
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1977
|
+
).filter((index) => index !== void 0);
|
|
1978
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1633
1979
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1634
1980
|
return {
|
|
1635
1981
|
ok: true,
|
|
@@ -1637,7 +1983,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1637
1983
|
status: "success",
|
|
1638
1984
|
value: await mergeSynthesisResults(orderedResults, {
|
|
1639
1985
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1640
|
-
signal: jobScope?.signal ??
|
|
1986
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
1641
1987
|
customMerger: options.customMerger,
|
|
1642
1988
|
outputMimeType: options.outputMimeType,
|
|
1643
1989
|
postMergeValidator: options.postMergeValidator
|
|
@@ -1648,6 +1994,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1648
1994
|
return failure(synthesisError, partialResultFrom(error));
|
|
1649
1995
|
} finally {
|
|
1650
1996
|
fallbackJobScope?.dispose();
|
|
1997
|
+
deadline.dispose();
|
|
1651
1998
|
}
|
|
1652
1999
|
}
|
|
1653
2000
|
function withValidationSignal(options, signal) {
|
|
@@ -1668,7 +2015,16 @@ var AzureTtsClient = class {
|
|
|
1668
2015
|
__privateSet(this, _options, options);
|
|
1669
2016
|
}
|
|
1670
2017
|
async synthesize(ssml) {
|
|
1671
|
-
const {
|
|
2018
|
+
const {
|
|
2019
|
+
region,
|
|
2020
|
+
subscriptionKey,
|
|
2021
|
+
outputFormat,
|
|
2022
|
+
signal,
|
|
2023
|
+
timeoutMs,
|
|
2024
|
+
timeouts,
|
|
2025
|
+
customHeaders,
|
|
2026
|
+
fingerprintSchemaVersion
|
|
2027
|
+
} = __privateGet(this, _options);
|
|
1672
2028
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1673
2029
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1674
2030
|
const config = {
|
|
@@ -1679,12 +2035,23 @@ var AzureTtsClient = class {
|
|
|
1679
2035
|
signal,
|
|
1680
2036
|
timeoutMs,
|
|
1681
2037
|
timeouts,
|
|
1682
|
-
retryOptions: __privateGet(this, _options).retryOptions
|
|
2038
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
2039
|
+
customHeaders,
|
|
2040
|
+
fingerprintSchemaVersion
|
|
1683
2041
|
};
|
|
1684
2042
|
return synthesizeSpeech(ssml, config);
|
|
1685
2043
|
}
|
|
1686
2044
|
async synthesizeSsml(ssml, options = {}) {
|
|
1687
|
-
const {
|
|
2045
|
+
const {
|
|
2046
|
+
region,
|
|
2047
|
+
subscriptionKey,
|
|
2048
|
+
outputFormat,
|
|
2049
|
+
signal,
|
|
2050
|
+
timeoutMs,
|
|
2051
|
+
timeouts,
|
|
2052
|
+
customHeaders,
|
|
2053
|
+
fingerprintSchemaVersion
|
|
2054
|
+
} = __privateGet(this, _options);
|
|
1688
2055
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1689
2056
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1690
2057
|
return synthesizeSsml(ssml, {
|
|
@@ -1703,11 +2070,22 @@ var AzureTtsClient = class {
|
|
|
1703
2070
|
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1704
2071
|
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1705
2072
|
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1706
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
2073
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2074
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2075
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1707
2076
|
});
|
|
1708
2077
|
}
|
|
1709
2078
|
async synthesizeChunks(chunks, options = {}) {
|
|
1710
|
-
const {
|
|
2079
|
+
const {
|
|
2080
|
+
region,
|
|
2081
|
+
subscriptionKey,
|
|
2082
|
+
outputFormat,
|
|
2083
|
+
signal,
|
|
2084
|
+
timeoutMs,
|
|
2085
|
+
timeouts,
|
|
2086
|
+
customHeaders,
|
|
2087
|
+
fingerprintSchemaVersion
|
|
2088
|
+
} = __privateGet(this, _options);
|
|
1711
2089
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1712
2090
|
return synthesizeSsmlChunks(chunks, {
|
|
1713
2091
|
endpoint,
|
|
@@ -1727,7 +2105,9 @@ var AzureTtsClient = class {
|
|
|
1727
2105
|
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1728
2106
|
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1729
2107
|
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1730
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
2108
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2109
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2110
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1731
2111
|
});
|
|
1732
2112
|
}
|
|
1733
2113
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1742,7 +2122,14 @@ var AzureTtsClient = class {
|
|
|
1742
2122
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1743
2123
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1744
2124
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1745
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
2125
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2126
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2127
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2128
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2129
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2130
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2131
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2132
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
1746
2133
|
});
|
|
1747
2134
|
}
|
|
1748
2135
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1848,6 +2235,8 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1848
2235
|
BatchChunkValidationError,
|
|
1849
2236
|
ChunkValidationError,
|
|
1850
2237
|
DEFAULT_OUTPUT_FORMAT,
|
|
2238
|
+
DeadlineController,
|
|
2239
|
+
IncompleteChunkSetError,
|
|
1851
2240
|
MergeError,
|
|
1852
2241
|
SynthesisCancelledError,
|
|
1853
2242
|
SynthesisTimeoutError,
|
|
@@ -1861,6 +2250,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1861
2250
|
mergeSynthesisResults,
|
|
1862
2251
|
resolveMergeAudioFormat,
|
|
1863
2252
|
resolveMimeType,
|
|
2253
|
+
serializeChunkError,
|
|
1864
2254
|
synthesizeSpeech,
|
|
1865
2255
|
synthesizeSsml,
|
|
1866
2256
|
synthesizeSsmlChunks,
|