@ssml-builder-js/azure-tts-client 2.18.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/index.d.mts +156 -105
- package/dist/index.d.ts +156 -105
- package/dist/index.js +453 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +450 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- 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/v219-pipeline.test.ts +131 -0
package/dist/index.mjs
CHANGED
|
@@ -69,6 +69,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
69
69
|
this.name = "SynthesisTimeoutError";
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
var IncompleteChunkSetError = class extends Error {
|
|
73
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
74
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
75
|
+
this.kind = "incomplete-chunk-set";
|
|
76
|
+
this.name = "IncompleteChunkSetError";
|
|
77
|
+
this.totalChunks = totalChunks;
|
|
78
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
79
|
+
}
|
|
80
|
+
};
|
|
72
81
|
var MergeError = class extends Error {
|
|
73
82
|
constructor(message, cause) {
|
|
74
83
|
super(message);
|
|
@@ -93,8 +102,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
93
102
|
this.format = format;
|
|
94
103
|
}
|
|
95
104
|
};
|
|
105
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
108
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
109
|
+
const code = kind === "validation-error" ? "VALIDATION_ERROR" : kind === "timeout" || /tim(?:e|ed) ?out|deadline/i.test(message) ? "TIMEOUT" : kind === "cancelled" || /cancel|abort/i.test(message) ? "CANCELLED" : kind === "audio-format-mismatch" || kind === "unsupported-format-error" ? "FORMAT_MISMATCH" : kind === "merge-error" || phase === "merge" ? "MERGE_ERROR" : "AZURE_API_ERROR";
|
|
110
|
+
const details = {};
|
|
111
|
+
if (error instanceof AzureTtsError) {
|
|
112
|
+
details.statusText = error.statusText;
|
|
113
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
114
|
+
}
|
|
115
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
116
|
+
details.totalChunks = error.totalChunks;
|
|
117
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
code,
|
|
121
|
+
phase,
|
|
122
|
+
message,
|
|
123
|
+
isOriginalFailure,
|
|
124
|
+
isRetryable: code === "AZURE_API_ERROR" && (status === 429 || status !== void 0 && status >= 500 || /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message)),
|
|
125
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
126
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
96
129
|
function toSynthesisError(error) {
|
|
97
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
130
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
98
131
|
return error;
|
|
99
132
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
133
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -106,6 +139,55 @@ function createSpeechSdkError(error) {
|
|
|
106
139
|
return new AzureTtsSdkError(message);
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
// src/deadline.ts
|
|
143
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
144
|
+
var DeadlineController = class {
|
|
145
|
+
constructor(totalJobMs, parent) {
|
|
146
|
+
__privateAdd(this, _controller, new AbortController());
|
|
147
|
+
__privateAdd(this, _parent);
|
|
148
|
+
__privateAdd(this, _onParentAbort);
|
|
149
|
+
__privateAdd(this, _timer);
|
|
150
|
+
__privateAdd(this, _timedOut, false);
|
|
151
|
+
__privateSet(this, _parent, parent);
|
|
152
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
153
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
154
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
155
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
156
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
157
|
+
if (this.deadlineAtMs !== void 0) {
|
|
158
|
+
__privateSet(this, _timer, setTimeout(
|
|
159
|
+
() => {
|
|
160
|
+
__privateSet(this, _timedOut, true);
|
|
161
|
+
__privateGet(this, _controller).abort();
|
|
162
|
+
},
|
|
163
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
get timedOut() {
|
|
168
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
169
|
+
}
|
|
170
|
+
get remainingMs() {
|
|
171
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
172
|
+
}
|
|
173
|
+
throwIfExpired() {
|
|
174
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
175
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
176
|
+
}
|
|
177
|
+
abort() {
|
|
178
|
+
__privateGet(this, _controller).abort();
|
|
179
|
+
}
|
|
180
|
+
dispose() {
|
|
181
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
182
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
_controller = new WeakMap();
|
|
186
|
+
_parent = new WeakMap();
|
|
187
|
+
_onParentAbort = new WeakMap();
|
|
188
|
+
_timer = new WeakMap();
|
|
189
|
+
_timedOut = new WeakMap();
|
|
190
|
+
|
|
109
191
|
// src/synthesis.ts
|
|
110
192
|
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
111
193
|
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
@@ -188,18 +270,23 @@ function createSpeechConfig(config) {
|
|
|
188
270
|
}
|
|
189
271
|
|
|
190
272
|
// src/synthesis.ts
|
|
191
|
-
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT) {
|
|
273
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
192
274
|
const readAttribute = (name) => {
|
|
193
275
|
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
194
276
|
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
195
277
|
};
|
|
278
|
+
const headers = Object.fromEntries(
|
|
279
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
280
|
+
);
|
|
196
281
|
const payload = JSON.stringify({
|
|
197
282
|
ssml,
|
|
198
283
|
outputFormat,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
284
|
+
region: options.region ?? "",
|
|
285
|
+
endpoint: options.endpoint ?? "",
|
|
286
|
+
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
287
|
+
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
288
|
+
customHeaders: headers,
|
|
289
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
203
290
|
});
|
|
204
291
|
let hash = 0xcbf29ce484222325n;
|
|
205
292
|
const mask = 0xffffffffffffffffn;
|
|
@@ -319,6 +406,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
319
406
|
}
|
|
320
407
|
return void 0;
|
|
321
408
|
}
|
|
409
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
410
|
+
const first = bytes[offset];
|
|
411
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
412
|
+
let mask = 128;
|
|
413
|
+
let length = 1;
|
|
414
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
415
|
+
mask >>= 1;
|
|
416
|
+
length += 1;
|
|
417
|
+
}
|
|
418
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
419
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
420
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
421
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
422
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
423
|
+
return { value, length };
|
|
424
|
+
}
|
|
425
|
+
function readEbmlElement(bytes, offset) {
|
|
426
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
427
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
428
|
+
const dataStart = offset + id.length + size.length;
|
|
429
|
+
const dataEnd = dataStart + size.value;
|
|
430
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
431
|
+
return { id: id.value, dataStart, dataEnd };
|
|
432
|
+
}
|
|
433
|
+
function ebmlText(bytes, element) {
|
|
434
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
435
|
+
}
|
|
436
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
437
|
+
let offset = start;
|
|
438
|
+
while (offset < end) {
|
|
439
|
+
const element = readEbmlElement(bytes, offset);
|
|
440
|
+
if (element.id === id) return element;
|
|
441
|
+
offset = element.dataEnd;
|
|
442
|
+
}
|
|
443
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
function parseOggSpecification(buffer, format) {
|
|
447
|
+
const bytes = new Uint8Array(buffer);
|
|
448
|
+
let offset = 0;
|
|
449
|
+
let firstPayload;
|
|
450
|
+
let pages = 0;
|
|
451
|
+
while (offset < bytes.byteLength) {
|
|
452
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
453
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
454
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
455
|
+
const lacingStart = offset + 27;
|
|
456
|
+
const payloadStart = lacingStart + segmentCount;
|
|
457
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
458
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
459
|
+
const pageEnd = payloadStart + payloadLength;
|
|
460
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
461
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
462
|
+
offset = pageEnd;
|
|
463
|
+
pages += 1;
|
|
464
|
+
}
|
|
465
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
466
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
467
|
+
const version = firstPayload[8];
|
|
468
|
+
const channels = firstPayload[9] ?? 0;
|
|
469
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
470
|
+
12,
|
|
471
|
+
true
|
|
472
|
+
);
|
|
473
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
474
|
+
return {
|
|
475
|
+
format,
|
|
476
|
+
mimeType: "audio/ogg",
|
|
477
|
+
codec: "opus",
|
|
478
|
+
sampleRate,
|
|
479
|
+
channels,
|
|
480
|
+
container: "ogg",
|
|
481
|
+
isVbr: true,
|
|
482
|
+
isCompressed: true
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseWebmSpecification(buffer, format) {
|
|
486
|
+
const bytes = new Uint8Array(buffer);
|
|
487
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
488
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
489
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
490
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
491
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
492
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
493
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
494
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
495
|
+
let offset = tracks.dataStart;
|
|
496
|
+
let opusTrack;
|
|
497
|
+
while (offset < tracks.dataEnd) {
|
|
498
|
+
const track = readEbmlElement(bytes, offset);
|
|
499
|
+
if (track.id === 174) {
|
|
500
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
501
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
502
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
503
|
+
opusTrack = track;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
offset = track.dataEnd;
|
|
508
|
+
}
|
|
509
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
510
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
511
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
512
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
513
|
+
const sampleRate = sampling ? new DataView(
|
|
514
|
+
bytes.buffer,
|
|
515
|
+
bytes.byteOffset + sampling.dataStart,
|
|
516
|
+
sampling.dataEnd - sampling.dataStart
|
|
517
|
+
).getFloat64(0, false) : 0;
|
|
518
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
519
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
520
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
521
|
+
return {
|
|
522
|
+
format,
|
|
523
|
+
mimeType: "audio/webm",
|
|
524
|
+
codec: "opus",
|
|
525
|
+
sampleRate: Math.round(sampleRate),
|
|
526
|
+
channels: channelCount,
|
|
527
|
+
container: "webm",
|
|
528
|
+
isVbr: true,
|
|
529
|
+
isCompressed: true
|
|
530
|
+
};
|
|
531
|
+
}
|
|
322
532
|
function inspectAudioSpecification(buffer, format) {
|
|
323
533
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
324
534
|
const parsed = parseWav(buffer);
|
|
@@ -343,7 +553,20 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
343
553
|
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
344
554
|
};
|
|
345
555
|
}
|
|
346
|
-
if (isMp3Format(format))
|
|
556
|
+
if (isMp3Format(format)) {
|
|
557
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
558
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
559
|
+
}
|
|
560
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
561
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
562
|
+
validateContainerFormat(specification2, format);
|
|
563
|
+
return specification2;
|
|
564
|
+
}
|
|
565
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
566
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
567
|
+
validateContainerFormat(specification2, format);
|
|
568
|
+
return specification2;
|
|
569
|
+
}
|
|
347
570
|
const specification = formatAudioSpecification(format);
|
|
348
571
|
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
349
572
|
return specification;
|
|
@@ -352,7 +575,20 @@ function validateRawAudioBuffer(buffer, specification) {
|
|
|
352
575
|
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
353
576
|
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
354
577
|
}
|
|
355
|
-
if (specification.codec === "siren"
|
|
578
|
+
if (specification.codec === "siren") return;
|
|
579
|
+
if (specification.codec === "silk") {
|
|
580
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
581
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
585
|
+
if (specification.codec === "opus") {
|
|
586
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
587
|
+
const frameCountCode = packetCode & 3;
|
|
588
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
589
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
356
592
|
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
357
593
|
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
358
594
|
throw new Error(
|
|
@@ -360,6 +596,15 @@ function validateRawAudioBuffer(buffer, specification) {
|
|
|
360
596
|
);
|
|
361
597
|
}
|
|
362
598
|
}
|
|
599
|
+
function validateContainerFormat(specification, format) {
|
|
600
|
+
const expected = formatAudioSpecification(format);
|
|
601
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
602
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
603
|
+
expected,
|
|
604
|
+
specification
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
363
608
|
function validateAudioSpecifications(specs) {
|
|
364
609
|
const first = specs[0];
|
|
365
610
|
if (!first) return;
|
|
@@ -441,17 +686,29 @@ function stripMp3Tags(buffer) {
|
|
|
441
686
|
function isMp3Format(format) {
|
|
442
687
|
return /(?:mp3|mpeg)/i.test(format);
|
|
443
688
|
}
|
|
689
|
+
function isOggFormat(format) {
|
|
690
|
+
return /ogg/i.test(format);
|
|
691
|
+
}
|
|
692
|
+
function isWebmFormat(format) {
|
|
693
|
+
return /webm/i.test(format);
|
|
694
|
+
}
|
|
444
695
|
function isWavFormat(format) {
|
|
445
696
|
return /(?:wav|wave|riff)/i.test(format);
|
|
446
697
|
}
|
|
447
698
|
function isRawFormat(format) {
|
|
448
699
|
return /^raw(?:-|$)/i.test(format);
|
|
449
700
|
}
|
|
450
|
-
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType) {
|
|
701
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
451
702
|
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
452
703
|
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
453
704
|
}
|
|
454
|
-
|
|
705
|
+
let specification;
|
|
706
|
+
try {
|
|
707
|
+
specification = inspectAudioSpecification(merged, format);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (!allowExternalContainer) throw error;
|
|
710
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
711
|
+
}
|
|
455
712
|
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
456
713
|
const firstInput = inputSpecs[0];
|
|
457
714
|
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
@@ -471,6 +728,25 @@ function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMi
|
|
|
471
728
|
}
|
|
472
729
|
return specification;
|
|
473
730
|
}
|
|
731
|
+
async function withinDeadline(value, deadline) {
|
|
732
|
+
if (!deadline) return value;
|
|
733
|
+
deadline.throwIfExpired();
|
|
734
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
735
|
+
let timer;
|
|
736
|
+
try {
|
|
737
|
+
return await Promise.race([
|
|
738
|
+
Promise.resolve(value),
|
|
739
|
+
new Promise((_resolve, reject) => {
|
|
740
|
+
timer = setTimeout(
|
|
741
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
742
|
+
deadline.remainingMs
|
|
743
|
+
);
|
|
744
|
+
})
|
|
745
|
+
]);
|
|
746
|
+
} finally {
|
|
747
|
+
if (timer) clearTimeout(timer);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
474
750
|
function resolveMergeAudioFormat(format) {
|
|
475
751
|
if (isWavFormat(format)) return "wav";
|
|
476
752
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -483,6 +759,7 @@ function canMergeAudioFormat(format) {
|
|
|
483
759
|
function mergeAudioBuffers(buffers, options) {
|
|
484
760
|
const format = typeof options === "string" ? options : options?.format;
|
|
485
761
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
762
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
486
763
|
try {
|
|
487
764
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
488
765
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -791,10 +1068,23 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadline
|
|
|
791
1068
|
}
|
|
792
1069
|
}
|
|
793
1070
|
async function synthesizeSsml(ssml, config) {
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1071
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1072
|
+
try {
|
|
1073
|
+
deadline.throwIfExpired();
|
|
1074
|
+
const synthesisConfig = {
|
|
1075
|
+
...config,
|
|
1076
|
+
signal: deadline.signal,
|
|
1077
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1078
|
+
};
|
|
1079
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1080
|
+
deadline.throwIfExpired();
|
|
1081
|
+
return result;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1084
|
+
throw error;
|
|
1085
|
+
} finally {
|
|
1086
|
+
deadline.dispose();
|
|
1087
|
+
}
|
|
798
1088
|
}
|
|
799
1089
|
function createAbortScope(parent, timeoutMs) {
|
|
800
1090
|
const controller = new AbortController();
|
|
@@ -831,7 +1121,12 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
831
1121
|
const totalChunks = chunks.length;
|
|
832
1122
|
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
833
1123
|
const fingerprints = inputs.map(
|
|
834
|
-
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT
|
|
1124
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1125
|
+
region: config.region,
|
|
1126
|
+
endpoint: config.endpoint,
|
|
1127
|
+
customHeaders: config.customHeaders,
|
|
1128
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1129
|
+
})
|
|
835
1130
|
);
|
|
836
1131
|
const results = new Array(totalChunks);
|
|
837
1132
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
@@ -855,6 +1150,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
855
1150
|
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
856
1151
|
const jobStartedAt = Date.now();
|
|
857
1152
|
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1153
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
858
1154
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
859
1155
|
const report = (event) => config.onProgress?.(event);
|
|
860
1156
|
for (const [index, input] of inputs.entries()) {
|
|
@@ -895,7 +1191,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
895
1191
|
input.ssml,
|
|
896
1192
|
{
|
|
897
1193
|
...config,
|
|
898
|
-
signal:
|
|
1194
|
+
signal: deadline.signal,
|
|
899
1195
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
900
1196
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
901
1197
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -940,7 +1236,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
940
1236
|
status: wasCancelled ? "cancelled" : "failed",
|
|
941
1237
|
isOriginalFailure: !wasCancelled,
|
|
942
1238
|
canResume: true,
|
|
943
|
-
error
|
|
1239
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
944
1240
|
};
|
|
945
1241
|
report({
|
|
946
1242
|
currentChunk: completed,
|
|
@@ -960,13 +1256,19 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
960
1256
|
try {
|
|
961
1257
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
962
1258
|
if (firstError) throw firstError;
|
|
1259
|
+
const missingChunkIndices = Array.from(
|
|
1260
|
+
{ length: totalChunks },
|
|
1261
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1262
|
+
).filter((index) => index !== void 0);
|
|
1263
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
963
1264
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
964
1265
|
return await mergeSynthesisResults(orderedResults, {
|
|
965
1266
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
966
|
-
signal:
|
|
1267
|
+
signal: deadline.signal,
|
|
967
1268
|
customMerger: config.customMerger,
|
|
968
1269
|
outputMimeType: config.outputMimeType,
|
|
969
|
-
postMergeValidator: config.postMergeValidator
|
|
1270
|
+
postMergeValidator: config.postMergeValidator,
|
|
1271
|
+
deadline
|
|
970
1272
|
});
|
|
971
1273
|
} catch (error) {
|
|
972
1274
|
if (firstError && config.cancelOnFailure !== false) {
|
|
@@ -994,6 +1296,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
994
1296
|
throw error;
|
|
995
1297
|
} finally {
|
|
996
1298
|
scope.dispose();
|
|
1299
|
+
deadline.dispose();
|
|
997
1300
|
}
|
|
998
1301
|
}
|
|
999
1302
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -1071,19 +1374,33 @@ function mergeSynthesisResults(results, options) {
|
|
|
1071
1374
|
const format = resolvedOptions?.format;
|
|
1072
1375
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
1073
1376
|
const buffers = results.map((result) => result.audioData);
|
|
1074
|
-
const inputSpecs = results.map((result) =>
|
|
1377
|
+
const inputSpecs = results.map((result) => {
|
|
1378
|
+
if (result.audioSpec) return result.audioSpec;
|
|
1379
|
+
try {
|
|
1380
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1075
1386
|
validateAudioSpecifications(inputSpecs);
|
|
1076
|
-
const
|
|
1387
|
+
const deadline = resolvedOptions.deadline;
|
|
1388
|
+
deadline?.throwIfExpired();
|
|
1389
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
1077
1390
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1078
1391
|
if (resolvedOptions.customMerger) {
|
|
1079
|
-
return
|
|
1080
|
-
()
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1392
|
+
return withinDeadline(
|
|
1393
|
+
Promise.resolve().then(
|
|
1394
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1395
|
+
format,
|
|
1396
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1397
|
+
inputSpecs,
|
|
1398
|
+
signal
|
|
1399
|
+
})
|
|
1400
|
+
),
|
|
1401
|
+
deadline
|
|
1086
1402
|
).then((merged) => {
|
|
1403
|
+
deadline?.throwIfExpired();
|
|
1087
1404
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
1088
1405
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1089
1406
|
const mergedSpec = validateMergedAudioBuffer(
|
|
@@ -1091,7 +1408,8 @@ function mergeSynthesisResults(results, options) {
|
|
|
1091
1408
|
format,
|
|
1092
1409
|
buffers,
|
|
1093
1410
|
inputSpecs,
|
|
1094
|
-
resolvedOptions.outputMimeType ?? resolveMimeType(format)
|
|
1411
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1412
|
+
true
|
|
1095
1413
|
);
|
|
1096
1414
|
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
1097
1415
|
return Promise.resolve(
|
|
@@ -1102,6 +1420,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1102
1420
|
signal
|
|
1103
1421
|
})
|
|
1104
1422
|
).then((valid) => {
|
|
1423
|
+
deadline?.throwIfExpired();
|
|
1105
1424
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1106
1425
|
return result;
|
|
1107
1426
|
});
|
|
@@ -1112,6 +1431,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
1112
1431
|
});
|
|
1113
1432
|
}
|
|
1114
1433
|
try {
|
|
1434
|
+
deadline?.throwIfExpired();
|
|
1115
1435
|
const result = createMergedResult(
|
|
1116
1436
|
results,
|
|
1117
1437
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -1127,12 +1447,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
1127
1447
|
signal
|
|
1128
1448
|
});
|
|
1129
1449
|
if (validation instanceof Promise)
|
|
1130
|
-
return validation.then((valid) => {
|
|
1450
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
1451
|
+
deadline?.throwIfExpired();
|
|
1131
1452
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1132
1453
|
return result;
|
|
1133
1454
|
});
|
|
1134
1455
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1135
1456
|
}
|
|
1457
|
+
deadline?.throwIfExpired();
|
|
1136
1458
|
return result;
|
|
1137
1459
|
} catch (error) {
|
|
1138
1460
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -1281,47 +1603,55 @@ function sharedValidationOptions(options, signal) {
|
|
|
1281
1603
|
};
|
|
1282
1604
|
}
|
|
1283
1605
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1284
|
-
const
|
|
1606
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1607
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1285
1608
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
1286
|
-
if (
|
|
1287
|
-
const error = toSynthesisError(
|
|
1609
|
+
if (deadline.signal.aborted) {
|
|
1610
|
+
const error = toSynthesisError(
|
|
1611
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1612
|
+
);
|
|
1613
|
+
deadline.dispose();
|
|
1288
1614
|
return failure(error);
|
|
1289
1615
|
}
|
|
1290
1616
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1291
1617
|
if (errors.length > 0) {
|
|
1618
|
+
deadline.dispose();
|
|
1292
1619
|
return failure({
|
|
1293
1620
|
kind: "validation-error",
|
|
1294
1621
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
1295
1622
|
diagnostics: errors
|
|
1296
1623
|
});
|
|
1297
1624
|
}
|
|
1298
|
-
const jobScope = options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts.totalJobMs) : void 0;
|
|
1299
1625
|
try {
|
|
1300
1626
|
return {
|
|
1301
1627
|
ok: true,
|
|
1302
1628
|
success: true,
|
|
1303
1629
|
status: "success",
|
|
1304
1630
|
value: await client.synthesizeSsml(ssml, {
|
|
1305
|
-
signal:
|
|
1631
|
+
signal: deadline.signal,
|
|
1306
1632
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1307
|
-
timeouts: options.timeouts
|
|
1633
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1308
1634
|
})
|
|
1309
1635
|
};
|
|
1310
1636
|
} catch (error) {
|
|
1311
|
-
if (
|
|
1637
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1312
1638
|
const synthesisError = toSynthesisError(error);
|
|
1313
1639
|
return failure(synthesisError);
|
|
1314
1640
|
} finally {
|
|
1315
|
-
|
|
1641
|
+
deadline.dispose();
|
|
1316
1642
|
}
|
|
1317
1643
|
}
|
|
1318
1644
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1645
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1319
1646
|
const validationOptions = sharedValidationOptions(
|
|
1320
1647
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1321
|
-
|
|
1648
|
+
deadline.signal
|
|
1322
1649
|
);
|
|
1323
|
-
if (
|
|
1324
|
-
const error = toSynthesisError(
|
|
1650
|
+
if (deadline.signal.aborted) {
|
|
1651
|
+
const error = toSynthesisError(
|
|
1652
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1653
|
+
);
|
|
1654
|
+
deadline.dispose();
|
|
1325
1655
|
return failure(error);
|
|
1326
1656
|
}
|
|
1327
1657
|
const pending = (index, status, error) => {
|
|
@@ -1353,14 +1683,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1353
1683
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1354
1684
|
})
|
|
1355
1685
|
);
|
|
1356
|
-
if (
|
|
1686
|
+
if (deadline.signal.aborted) {
|
|
1357
1687
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1688
|
+
deadline.dispose();
|
|
1358
1689
|
return failure(error);
|
|
1359
1690
|
}
|
|
1360
1691
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1361
1692
|
if (chunkDiagnostics.length > 0) {
|
|
1362
1693
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1363
1694
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1695
|
+
deadline.dispose();
|
|
1364
1696
|
return failure(error);
|
|
1365
1697
|
}
|
|
1366
1698
|
let fallbackJobScope;
|
|
@@ -1373,9 +1705,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1373
1705
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1374
1706
|
onProgress: options.onProgress,
|
|
1375
1707
|
outputFormat: options.outputFormat,
|
|
1376
|
-
signal:
|
|
1708
|
+
signal: deadline.signal,
|
|
1377
1709
|
timeoutMs: options.timeoutMs,
|
|
1378
|
-
timeouts: options.timeouts,
|
|
1710
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1379
1711
|
sourceNodePath: options.sourceNodePath,
|
|
1380
1712
|
concurrency: options.concurrency,
|
|
1381
1713
|
retryOptions: options.retryOptions,
|
|
@@ -1385,12 +1717,19 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1385
1717
|
customMerger: options.customMerger,
|
|
1386
1718
|
outputMimeType: options.outputMimeType,
|
|
1387
1719
|
postMergeValidator: options.postMergeValidator,
|
|
1388
|
-
resumeValidation: options.resumeValidation
|
|
1720
|
+
resumeValidation: options.resumeValidation,
|
|
1721
|
+
customHeaders: options.customHeaders,
|
|
1722
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1389
1723
|
});
|
|
1390
1724
|
return { ok: true, success: true, status: "success", value };
|
|
1391
1725
|
}
|
|
1392
1726
|
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1393
|
-
const fingerprints = inputs.map(
|
|
1727
|
+
const fingerprints = inputs.map(
|
|
1728
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1729
|
+
customHeaders: options.customHeaders,
|
|
1730
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1731
|
+
})
|
|
1732
|
+
);
|
|
1394
1733
|
const results = new Array(chunks.length);
|
|
1395
1734
|
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1396
1735
|
chunkIndex,
|
|
@@ -1408,9 +1747,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1408
1747
|
}
|
|
1409
1748
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1410
1749
|
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1411
|
-
const
|
|
1412
|
-
const
|
|
1413
|
-
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1750
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1751
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1414
1752
|
fallbackJobScope = jobScope;
|
|
1415
1753
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
1416
1754
|
let firstError;
|
|
@@ -1434,8 +1772,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1434
1772
|
const startedAt = Date.now();
|
|
1435
1773
|
try {
|
|
1436
1774
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1437
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
1438
|
-
const chunkSignal = chunkScope?.signal ??
|
|
1775
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1776
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1439
1777
|
let result;
|
|
1440
1778
|
try {
|
|
1441
1779
|
result = await retryableSynthesis(
|
|
@@ -1462,7 +1800,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1462
1800
|
jobDeadlineAt
|
|
1463
1801
|
);
|
|
1464
1802
|
} catch (error) {
|
|
1465
|
-
if (chunkScope?.timedOut())
|
|
1803
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1466
1804
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1467
1805
|
throw error;
|
|
1468
1806
|
} finally {
|
|
@@ -1523,15 +1861,15 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1523
1861
|
durationMs: Date.now() - startedAt
|
|
1524
1862
|
});
|
|
1525
1863
|
} catch (error) {
|
|
1526
|
-
const wasCancelled = firstError !== void 0 || Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1527
|
-
firstError ?? (firstError = error);
|
|
1864
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1865
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1528
1866
|
if (!wasCancelled) failedIndices.add(index);
|
|
1529
1867
|
chunkStates[index] = {
|
|
1530
1868
|
chunkIndex: index,
|
|
1531
1869
|
status: wasCancelled ? "cancelled" : "failed",
|
|
1532
1870
|
isOriginalFailure: !wasCancelled,
|
|
1533
1871
|
canResume: true,
|
|
1534
|
-
error
|
|
1872
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1535
1873
|
};
|
|
1536
1874
|
options.onProgress?.({
|
|
1537
1875
|
currentChunk: completed,
|
|
@@ -1574,6 +1912,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1574
1912
|
};
|
|
1575
1913
|
throw error;
|
|
1576
1914
|
}
|
|
1915
|
+
const missingChunkIndices = Array.from(
|
|
1916
|
+
{ length: chunks.length },
|
|
1917
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1918
|
+
).filter((index) => index !== void 0);
|
|
1919
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1577
1920
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
1578
1921
|
return {
|
|
1579
1922
|
ok: true,
|
|
@@ -1581,7 +1924,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1581
1924
|
status: "success",
|
|
1582
1925
|
value: await mergeSynthesisResults(orderedResults, {
|
|
1583
1926
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1584
|
-
signal: jobScope?.signal ??
|
|
1927
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
1585
1928
|
customMerger: options.customMerger,
|
|
1586
1929
|
outputMimeType: options.outputMimeType,
|
|
1587
1930
|
postMergeValidator: options.postMergeValidator
|
|
@@ -1592,6 +1935,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1592
1935
|
return failure(synthesisError, partialResultFrom(error));
|
|
1593
1936
|
} finally {
|
|
1594
1937
|
fallbackJobScope?.dispose();
|
|
1938
|
+
deadline.dispose();
|
|
1595
1939
|
}
|
|
1596
1940
|
}
|
|
1597
1941
|
function withValidationSignal(options, signal) {
|
|
@@ -1612,7 +1956,16 @@ var AzureTtsClient = class {
|
|
|
1612
1956
|
__privateSet(this, _options, options);
|
|
1613
1957
|
}
|
|
1614
1958
|
async synthesize(ssml) {
|
|
1615
|
-
const {
|
|
1959
|
+
const {
|
|
1960
|
+
region,
|
|
1961
|
+
subscriptionKey,
|
|
1962
|
+
outputFormat,
|
|
1963
|
+
signal,
|
|
1964
|
+
timeoutMs,
|
|
1965
|
+
timeouts,
|
|
1966
|
+
customHeaders,
|
|
1967
|
+
fingerprintSchemaVersion
|
|
1968
|
+
} = __privateGet(this, _options);
|
|
1616
1969
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1617
1970
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1618
1971
|
const config = {
|
|
@@ -1623,12 +1976,23 @@ var AzureTtsClient = class {
|
|
|
1623
1976
|
signal,
|
|
1624
1977
|
timeoutMs,
|
|
1625
1978
|
timeouts,
|
|
1626
|
-
retryOptions: __privateGet(this, _options).retryOptions
|
|
1979
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
1980
|
+
customHeaders,
|
|
1981
|
+
fingerprintSchemaVersion
|
|
1627
1982
|
};
|
|
1628
1983
|
return synthesizeSpeech(ssml, config);
|
|
1629
1984
|
}
|
|
1630
1985
|
async synthesizeSsml(ssml, options = {}) {
|
|
1631
|
-
const {
|
|
1986
|
+
const {
|
|
1987
|
+
region,
|
|
1988
|
+
subscriptionKey,
|
|
1989
|
+
outputFormat,
|
|
1990
|
+
signal,
|
|
1991
|
+
timeoutMs,
|
|
1992
|
+
timeouts,
|
|
1993
|
+
customHeaders,
|
|
1994
|
+
fingerprintSchemaVersion
|
|
1995
|
+
} = __privateGet(this, _options);
|
|
1632
1996
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1633
1997
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1634
1998
|
return synthesizeSsml(ssml, {
|
|
@@ -1647,11 +2011,22 @@ var AzureTtsClient = class {
|
|
|
1647
2011
|
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1648
2012
|
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1649
2013
|
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1650
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
2014
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2015
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2016
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1651
2017
|
});
|
|
1652
2018
|
}
|
|
1653
2019
|
async synthesizeChunks(chunks, options = {}) {
|
|
1654
|
-
const {
|
|
2020
|
+
const {
|
|
2021
|
+
region,
|
|
2022
|
+
subscriptionKey,
|
|
2023
|
+
outputFormat,
|
|
2024
|
+
signal,
|
|
2025
|
+
timeoutMs,
|
|
2026
|
+
timeouts,
|
|
2027
|
+
customHeaders,
|
|
2028
|
+
fingerprintSchemaVersion
|
|
2029
|
+
} = __privateGet(this, _options);
|
|
1655
2030
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1656
2031
|
return synthesizeSsmlChunks(chunks, {
|
|
1657
2032
|
endpoint,
|
|
@@ -1671,7 +2046,9 @@ var AzureTtsClient = class {
|
|
|
1671
2046
|
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1672
2047
|
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1673
2048
|
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1674
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
2049
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2050
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
2051
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
1675
2052
|
});
|
|
1676
2053
|
}
|
|
1677
2054
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1686,7 +2063,14 @@ var AzureTtsClient = class {
|
|
|
1686
2063
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1687
2064
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1688
2065
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1689
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
2066
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2067
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2068
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2069
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2070
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2071
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2072
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2073
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
1690
2074
|
});
|
|
1691
2075
|
}
|
|
1692
2076
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1791,6 +2175,8 @@ export {
|
|
|
1791
2175
|
BatchChunkValidationError,
|
|
1792
2176
|
ChunkValidationError,
|
|
1793
2177
|
DEFAULT_OUTPUT_FORMAT,
|
|
2178
|
+
DeadlineController,
|
|
2179
|
+
IncompleteChunkSetError,
|
|
1794
2180
|
MergeError,
|
|
1795
2181
|
SynthesisCancelledError,
|
|
1796
2182
|
SynthesisTimeoutError,
|
|
@@ -1804,6 +2190,7 @@ export {
|
|
|
1804
2190
|
mergeSynthesisResults,
|
|
1805
2191
|
resolveMergeAudioFormat,
|
|
1806
2192
|
resolveMimeType,
|
|
2193
|
+
serializeChunkError,
|
|
1807
2194
|
synthesizeSpeech,
|
|
1808
2195
|
synthesizeSsml,
|
|
1809
2196
|
synthesizeSsmlChunks,
|