@ssml-builder-js/azure-tts-client 2.17.0 → 2.19.0

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