ssml-builder-js 2.17.0 → 2.19.0

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