ssml-builder-js 2.16.0 → 2.17.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
@@ -18,12 +18,12 @@ import {
18
18
  validateAzureSsmlChunks,
19
19
  validateSsml,
20
20
  validateSsmlStructureIntegrity
21
- } from "./chunk-NKLZGITR.mjs";
21
+ } from "./chunk-2SYLELUT.mjs";
22
22
  import {
23
23
  createAzureUrlValidatorRunner as createAzureUrlValidatorRunner2,
24
24
  getSsmlSourceMap as getSsmlSourceMap2,
25
25
  validateAzureSsml as validateAzureSsml2
26
- } from "./chunk-HI74FTKY.mjs";
26
+ } from "./chunk-5AZWURHW.mjs";
27
27
  import {
28
28
  __privateAdd,
29
29
  __privateGet,
@@ -32,7 +32,7 @@ import {
32
32
 
33
33
  // packages/azure-tts-client/src/errors.ts
34
34
  var AzureTtsError = class extends Error {
35
- constructor(status, statusText, responseBody, requestId) {
35
+ constructor(status, statusText, responseBody, requestId, responseHeaders) {
36
36
  super(`Azure TTS request failed: ${status} ${statusText}`);
37
37
  this.kind = "azure-api-error";
38
38
  this.name = "AzureTtsError";
@@ -40,8 +40,37 @@ var AzureTtsError = class extends Error {
40
40
  this.statusText = statusText;
41
41
  this.responseBody = responseBody;
42
42
  this.requestId = requestId;
43
+ const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
44
+ const seconds = value ? Number(value.trim()) : NaN;
45
+ const date = value ? Date.parse(value) : NaN;
46
+ if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
47
+ else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
43
48
  }
44
49
  };
50
+ function getRetryAfterDelayMs(error) {
51
+ if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
52
+ if (!error || typeof error !== "object") return void 0;
53
+ const candidate = error;
54
+ if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
55
+ const headers = candidate.headers ?? candidate.response?.headers;
56
+ if (headers instanceof Headers) {
57
+ const value = headers.get("retry-after");
58
+ if (!value) return void 0;
59
+ const seconds = Number(value.trim());
60
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
61
+ const date = Date.parse(value);
62
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
63
+ }
64
+ if (headers && typeof headers === "object") {
65
+ const value = headers["retry-after"] ?? headers["Retry-After"];
66
+ if (typeof value !== "string") return void 0;
67
+ const seconds = Number(value.trim());
68
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
69
+ const date = Date.parse(value);
70
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
71
+ }
72
+ return void 0;
73
+ }
45
74
  var AzureTtsSdkError = class extends AzureTtsError {
46
75
  constructor(errorDetails) {
47
76
  super(0, "Speech SDK", errorDetails, null);
@@ -233,6 +262,8 @@ function formatAudioSpecification(format) {
233
262
  const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
234
263
  const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
235
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";
265
+ const bitDepthMatch = /(\d+)bit/i.exec(format);
266
+ 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;
236
267
  return {
237
268
  format,
238
269
  mimeType: resolveMimeType(format),
@@ -240,6 +271,9 @@ function formatAudioSpecification(format) {
240
271
  sampleRate,
241
272
  channels,
242
273
  ...bitrate ? { bitrate } : {},
274
+ ...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
275
+ ...container ? { container } : {},
276
+ isVbr: /vbr/i.test(format),
243
277
  isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
244
278
  };
245
279
  }
@@ -275,6 +309,8 @@ function parseMp3Specification(buffer, format) {
275
309
  sampleRate,
276
310
  channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
277
311
  bitrate: bitrateKbps * 1e3,
312
+ container: "mp3-raw",
313
+ isVbr: false,
278
314
  isCompressed: true
279
315
  };
280
316
  }
@@ -296,6 +332,9 @@ function inspectAudioSpecification(buffer, format) {
296
332
  sampleRate,
297
333
  channels,
298
334
  ...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
335
+ bitDepth: bitsPerSample,
336
+ container: "riff-wave",
337
+ isVbr: false,
299
338
  isCompressed: formatCode !== 1
300
339
  };
301
340
  }
@@ -306,7 +345,7 @@ function validateAudioSpecifications(specs) {
306
345
  const first = specs[0];
307
346
  if (!first) return;
308
347
  const mismatch = specs.find(
309
- (spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate
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
310
349
  );
311
350
  if (mismatch)
312
351
  throw new AudioFormatMismatchError(
@@ -509,9 +548,7 @@ async function synthesizeSsml(ssml, config) {
509
548
  };
510
549
  }
511
550
  if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
512
- const unmapped = { mappingStatus: "unmapped" };
513
- Object.defineProperty(unmapped, "mappingStatus", { value: "unmapped", enumerable: false });
514
- return unmapped;
551
+ return { mappingStatus: "unmapped" };
515
552
  }
516
553
  const value = text ?? "";
517
554
  let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
@@ -593,8 +630,13 @@ async function synthesizeSsml(ssml, config) {
593
630
  ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
594
631
  ...requestId ? { requestId } : {}
595
632
  };
596
- if (event.mappingStatus === "unmapped")
633
+ if (event.mappingStatus === "unmapped") {
597
634
  Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
635
+ Object.defineProperty(mapped, "toJSON", {
636
+ value: () => ({ ...mapped, mappingStatus: "unmapped" }),
637
+ enumerable: false
638
+ });
639
+ }
598
640
  return mapped;
599
641
  };
600
642
  const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
@@ -617,10 +659,11 @@ async function synthesizeSsml(ssml, config) {
617
659
  abortHandler = () => rejectWithError(new SynthesisCancelledError());
618
660
  config.signal.addEventListener("abort", abortHandler, { once: true });
619
661
  }
620
- if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
662
+ const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
663
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
621
664
  timeout = setTimeout(
622
- () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
623
- config.timeoutMs
665
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
666
+ timeoutMs
624
667
  );
625
668
  }
626
669
  synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
@@ -639,7 +682,9 @@ function isRetryableSynthesisError(error) {
639
682
  if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
640
683
  return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
641
684
  }
642
- function retryDelay(options, retryAttempt) {
685
+ function retryDelay(options, retryAttempt, error) {
686
+ const retryAfterMs = getRetryAfterDelayMs(error);
687
+ if (retryAfterMs !== void 0) return retryAfterMs;
643
688
  const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
644
689
  return Math.floor(Math.random() * (base + 1));
645
690
  }
@@ -671,7 +716,8 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
671
716
  const options = retryOptions ? {
672
717
  maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
673
718
  initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
674
- maxDelayMs: Math.max(0, retryOptions.maxDelayMs)
719
+ maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
720
+ shouldRetry: retryOptions.shouldRetry
675
721
  } : void 0;
676
722
  let attempt = 0;
677
723
  while (true) {
@@ -679,17 +725,56 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
679
725
  try {
680
726
  return await synthesizeSsml(ssml, config);
681
727
  } catch (error) {
682
- if (!options || attempt >= options.maxRetries || !isRetryableSynthesisError(error)) throw error;
728
+ if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
729
+ throw error;
683
730
  attempt += 1;
684
- const delayMs = retryDelay(options, attempt);
731
+ const delayMs = retryDelay(options, attempt, error);
685
732
  onRetry(attempt, delayMs);
686
733
  await waitForRetry(delayMs, config.signal);
687
734
  }
688
735
  }
689
736
  }
737
+ function createAbortScope(parent, timeoutMs) {
738
+ const controller = new AbortController();
739
+ let didTimeout = false;
740
+ const onAbort = () => controller.abort();
741
+ if (parent?.aborted) controller.abort();
742
+ parent?.addEventListener("abort", onAbort, { once: true });
743
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
744
+ didTimeout = true;
745
+ controller.abort();
746
+ }, timeoutMs) : void 0;
747
+ return {
748
+ signal: controller.signal,
749
+ timedOut: () => didTimeout,
750
+ dispose: () => {
751
+ if (timer) clearTimeout(timer);
752
+ parent?.removeEventListener("abort", onAbort);
753
+ },
754
+ abort: () => controller.abort()
755
+ };
756
+ }
757
+ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
758
+ const scope = createAbortScope(config.signal, timeoutMs);
759
+ try {
760
+ return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
761
+ } catch (error) {
762
+ if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
763
+ throw error;
764
+ } finally {
765
+ scope.dispose();
766
+ }
767
+ }
690
768
  async function synthesizeSsmlChunks(chunks, config) {
691
769
  const results = new Array(chunks.length);
692
770
  const totalChunks = chunks.length;
771
+ const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
772
+ for (const [index, cached] of cachedChunks) {
773
+ if (index >= 0 && index < totalChunks) results[index] = cached;
774
+ }
775
+ 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));
777
+ const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
693
778
  const report = (event) => config.onProgress?.(event);
694
779
  for (const [index, chunk] of chunks.entries()) {
695
780
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
@@ -703,13 +788,17 @@ async function synthesizeSsmlChunks(chunks, config) {
703
788
  durationMs: 0
704
789
  });
705
790
  }
706
- let completed = 0;
791
+ let completed = [...results].filter((result) => result !== void 0).length;
707
792
  let nextIndex = 0;
708
793
  const concurrency = resolveConcurrency(config.concurrency, chunks.length);
794
+ let firstError;
795
+ const failedIndices = /* @__PURE__ */ new Set();
709
796
  const worker = async () => {
710
797
  while (true) {
711
798
  const index = nextIndex++;
712
799
  if (index >= chunks.length) return;
800
+ if (!shouldSynthesize(index)) continue;
801
+ if (firstError && config.cancelOnFailure !== false) return;
713
802
  const chunk = chunks[index];
714
803
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
715
804
  report({
@@ -723,10 +812,11 @@ async function synthesizeSsmlChunks(chunks, config) {
723
812
  });
724
813
  const startedAt = Date.now();
725
814
  try {
726
- const result = await synthesizeWithRetry(
815
+ const result = await synthesizeChunkWithTimeout(
727
816
  input.ssml,
728
817
  {
729
818
  ...config,
819
+ signal: scope.signal,
730
820
  ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
731
821
  ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
732
822
  ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
@@ -735,6 +825,7 @@ async function synthesizeSsmlChunks(chunks, config) {
735
825
  onProgress: void 0
736
826
  },
737
827
  config.retryOptions,
828
+ config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
738
829
  (retryAttempt, nextRetryDelayMs) => report({
739
830
  currentChunk: completed,
740
831
  totalChunks,
@@ -760,6 +851,7 @@ async function synthesizeSsmlChunks(chunks, config) {
760
851
  durationMs: Date.now() - startedAt
761
852
  });
762
853
  } catch (error) {
854
+ failedIndices.add(index);
763
855
  report({
764
856
  currentChunk: completed,
765
857
  totalChunks,
@@ -770,16 +862,36 @@ async function synthesizeSsmlChunks(chunks, config) {
770
862
  durationMs: Date.now() - startedAt,
771
863
  error
772
864
  });
773
- throw error;
865
+ firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
866
+ if (config.cancelOnFailure !== false) scope.abort();
867
+ return;
774
868
  }
775
869
  }
776
870
  };
777
- await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
778
- const orderedResults = results.filter((result) => result !== void 0);
779
- return mergeSynthesisResults(orderedResults, {
780
- format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
781
- signal: config.signal
782
- });
871
+ try {
872
+ await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
873
+ if (firstError) throw firstError;
874
+ const orderedResults = results.filter((result) => result !== void 0);
875
+ return await mergeSynthesisResults(orderedResults, {
876
+ format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
877
+ signal: scope.signal,
878
+ customMerger: config.customMerger,
879
+ outputMimeType: config.outputMimeType,
880
+ postMergeValidator: config.postMergeValidator
881
+ });
882
+ } catch (error) {
883
+ 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]),
887
+ failedChunkIndices: [...failedIndices],
888
+ totalChunks
889
+ };
890
+ if (error && typeof error === "object") error.partialResult = partial;
891
+ throw error;
892
+ } finally {
893
+ scope.dispose();
894
+ }
783
895
  }
784
896
  function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
785
897
  const boundaries = [];
@@ -873,13 +985,24 @@ function mergeSynthesisResults(results, options) {
873
985
  if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
874
986
  throw new MergeError("The custom audio merger returned an invalid audio buffer.");
875
987
  if (signal.aborted) throw new SynthesisCancelledError();
876
- return createMergedResult(
988
+ const result = createMergedResult(
877
989
  results,
878
990
  merged,
879
991
  format,
880
992
  inspectAudioSpecification(merged, format),
881
993
  resolvedOptions.outputMimeType
882
994
  );
995
+ return Promise.resolve(
996
+ resolvedOptions.postMergeValidator?.(result, {
997
+ format,
998
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
999
+ inputSpecs,
1000
+ signal
1001
+ })
1002
+ ).then((valid) => {
1003
+ if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
1004
+ return result;
1005
+ });
883
1006
  }).catch((error) => {
884
1007
  if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
885
1008
  throw error;
@@ -887,13 +1010,28 @@ function mergeSynthesisResults(results, options) {
887
1010
  });
888
1011
  }
889
1012
  try {
890
- return createMergedResult(
1013
+ const result = createMergedResult(
891
1014
  results,
892
1015
  mergeAudioBuffers(buffers, { format }),
893
1016
  format,
894
1017
  inputSpecs[0],
895
1018
  resolvedOptions.outputMimeType
896
1019
  );
1020
+ if (resolvedOptions.postMergeValidator) {
1021
+ const validation = resolvedOptions.postMergeValidator(result, {
1022
+ format,
1023
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
1024
+ inputSpecs,
1025
+ signal
1026
+ });
1027
+ if (validation instanceof Promise)
1028
+ return validation.then((valid) => {
1029
+ if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
1030
+ return result;
1031
+ });
1032
+ if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
1033
+ }
1034
+ return result;
897
1035
  } catch (error) {
898
1036
  if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
899
1037
  throw error;
@@ -914,8 +1052,52 @@ var ChunkValidationError = class extends Error {
914
1052
  this.diagnostics = diagnostics;
915
1053
  }
916
1054
  };
917
- function failure(error) {
918
- return { ok: false, success: false, status: error.kind, error };
1055
+ var BatchChunkValidationError = class extends ChunkValidationError {
1056
+ constructor(chunkDiagnostics) {
1057
+ const first = chunkDiagnostics[0];
1058
+ super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
1059
+ this.name = "BatchChunkValidationError";
1060
+ this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
1061
+ this.chunkDiagnostics = chunkDiagnostics;
1062
+ this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
1063
+ this.errorCount = this.totalErrorCount;
1064
+ this.totalErrors = this.totalErrorCount;
1065
+ }
1066
+ };
1067
+ function failure(error, partialResult) {
1068
+ return {
1069
+ ok: false,
1070
+ success: false,
1071
+ status: error.kind,
1072
+ error,
1073
+ ...partialResult ? { partialResult } : {}
1074
+ };
1075
+ }
1076
+ function partialResultFrom(error) {
1077
+ if (!error || typeof error !== "object") return void 0;
1078
+ const partial = error.partialResult;
1079
+ if (!partial || typeof partial !== "object") return void 0;
1080
+ return partial;
1081
+ }
1082
+ function createSafeAbortScope(parent, timeoutMs) {
1083
+ const controller = new AbortController();
1084
+ let didTimeout = false;
1085
+ const onAbort = () => controller.abort();
1086
+ if (parent?.aborted) controller.abort();
1087
+ parent?.addEventListener("abort", onAbort, { once: true });
1088
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
1089
+ didTimeout = true;
1090
+ controller.abort();
1091
+ }, timeoutMs) : void 0;
1092
+ return {
1093
+ signal: controller.signal,
1094
+ timedOut: () => didTimeout,
1095
+ dispose: () => {
1096
+ if (timer) clearTimeout(timer);
1097
+ parent?.removeEventListener("abort", onAbort);
1098
+ },
1099
+ abort: () => controller.abort()
1100
+ };
919
1101
  }
920
1102
  function isRetryable(error) {
921
1103
  if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
@@ -930,6 +1112,9 @@ function delayForRetry(options, attempt) {
930
1112
  const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
931
1113
  return Math.floor(Math.random() * (base + 1));
932
1114
  }
1115
+ function retryDelayForError(options, attempt, error) {
1116
+ return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
1117
+ }
933
1118
  function resolveConcurrency2(value, total) {
934
1119
  if (value === void 0) return 1;
935
1120
  if (value === Infinity) return Math.max(1, total);
@@ -939,7 +1124,8 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
939
1124
  const retry = options ? {
940
1125
  maxRetries: Math.max(0, Math.floor(options.maxRetries)),
941
1126
  initialDelayMs: options.initialDelayMs,
942
- maxDelayMs: options.maxDelayMs
1127
+ maxDelayMs: options.maxDelayMs,
1128
+ shouldRetry: options.shouldRetry
943
1129
  } : void 0;
944
1130
  let attempt = 0;
945
1131
  while (true) {
@@ -947,9 +1133,10 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
947
1133
  try {
948
1134
  return await synthesize();
949
1135
  } catch (error) {
950
- if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
1136
+ if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
1137
+ throw error;
951
1138
  attempt += 1;
952
- const delayMs = delayForRetry(retry, attempt);
1139
+ const delayMs = retryDelayForError(retry, attempt, error);
953
1140
  onRetry(attempt, delayMs);
954
1141
  if (delayMs > 0)
955
1142
  await new Promise((resolve, reject) => {
@@ -973,7 +1160,7 @@ function sharedValidationOptions(options, signal) {
973
1160
  const runner = createAzureUrlValidatorRunner2(validator, {
974
1161
  ...options.urlValidation ?? {},
975
1162
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
976
- ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
1163
+ ...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
977
1164
  ...signal ? { signal } : {},
978
1165
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
979
1166
  });
@@ -1002,7 +1189,11 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
1002
1189
  ok: true,
1003
1190
  success: true,
1004
1191
  status: "success",
1005
- value: await client.synthesizeSsml(ssml, { signal: options.signal })
1192
+ value: await client.synthesizeSsml(ssml, {
1193
+ signal: options.signal,
1194
+ timeoutMs: options.timeouts?.perChunkMs,
1195
+ timeouts: options.timeouts
1196
+ })
1006
1197
  };
1007
1198
  } catch (error) {
1008
1199
  const synthesisError = toSynthesisError(error);
@@ -1010,7 +1201,10 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
1010
1201
  }
1011
1202
  }
1012
1203
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1013
- const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
1204
+ const validationOptions = sharedValidationOptions(
1205
+ { ...options.validation ?? options, timeouts: options.timeouts },
1206
+ options.signal
1207
+ );
1014
1208
  if (options.signal?.aborted) {
1015
1209
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
1016
1210
  return failure(error);
@@ -1044,16 +1238,17 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1044
1238
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
1045
1239
  })
1046
1240
  );
1047
- const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
1048
1241
  if (options.signal?.aborted) {
1049
1242
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
1050
1243
  return failure(error);
1051
1244
  }
1052
- if (firstInvalidIndex >= 0) {
1053
- const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
1054
- pending(firstInvalidIndex, "failed", error);
1245
+ const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
1246
+ if (chunkDiagnostics.length > 0) {
1247
+ const error = new BatchChunkValidationError(chunkDiagnostics);
1248
+ for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
1055
1249
  return failure(error);
1056
1250
  }
1251
+ let fallbackJobScope;
1057
1252
  try {
1058
1253
  if (client.synthesizeChunks) {
1059
1254
  const normalizedChunks = chunks.map((chunk) => {
@@ -1065,20 +1260,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1065
1260
  outputFormat: options.outputFormat,
1066
1261
  signal: options.signal,
1067
1262
  timeoutMs: options.timeoutMs,
1263
+ timeouts: options.timeouts,
1068
1264
  sourceNodePath: options.sourceNodePath,
1069
1265
  concurrency: options.concurrency,
1070
- retryOptions: options.retryOptions
1266
+ retryOptions: options.retryOptions,
1267
+ cancelOnFailure: options.cancelOnFailure,
1268
+ resumeChunks: options.resumeChunks,
1269
+ resumeChunkIndices: options.resumeChunkIndices,
1270
+ customMerger: options.customMerger,
1271
+ outputMimeType: options.outputMimeType,
1272
+ postMergeValidator: options.postMergeValidator
1071
1273
  });
1072
1274
  return { ok: true, success: true, status: "success", value };
1073
1275
  }
1074
1276
  const results = new Array(chunks.length);
1075
- let completed = 0;
1277
+ const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
1278
+ for (const [index, cached] of cachedChunks) {
1279
+ if (index >= 0 && index < chunks.length) results[index] = cached;
1280
+ }
1281
+ 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;
1284
+ fallbackJobScope = jobScope;
1285
+ const failedIndices = /* @__PURE__ */ new Set();
1286
+ let firstError;
1287
+ let completed = [...results].filter((result) => result !== void 0).length;
1076
1288
  let nextIndex = 0;
1077
1289
  const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
1078
1290
  const worker = async () => {
1079
1291
  while (true) {
1080
1292
  const index = nextIndex++;
1081
1293
  if (index >= chunks.length) return;
1294
+ if (!shouldSynthesize(index)) continue;
1295
+ if (failedIndices.size > 0 && options.cancelOnFailure !== false) return;
1082
1296
  const chunk = chunks[index];
1083
1297
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
1084
1298
  const sourceNodePath = input.sourceNodePath;
@@ -1086,28 +1300,40 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1086
1300
  pending(index, "synthesizing");
1087
1301
  const startedAt = Date.now();
1088
1302
  try {
1089
- const result = await retryableSynthesis(
1090
- () => client.synthesizeSsml(input.ssml, {
1091
- outputFormat: options.outputFormat,
1092
- signal: options.signal,
1093
- timeoutMs: options.timeoutMs,
1094
- sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
1095
- }),
1096
- options.retryOptions,
1097
- options.signal,
1098
- (retryAttempt, nextRetryDelayMs) => options.onProgress?.({
1099
- currentChunk: completed,
1100
- totalChunks: chunks.length,
1101
- percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
1102
- chunkIndex: index,
1103
- originalTextRange: input.originalTextRange,
1104
- status: "synthesizing",
1105
- durationMs: Date.now() - startedAt,
1106
- retryAttempt,
1107
- nextRetryDelayMs,
1108
- isRetrying: true
1109
- })
1110
- );
1303
+ 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;
1306
+ let result;
1307
+ try {
1308
+ result = await retryableSynthesis(
1309
+ () => client.synthesizeSsml(input.ssml, {
1310
+ outputFormat: options.outputFormat,
1311
+ signal: chunkSignal,
1312
+ timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
1313
+ sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
1314
+ }),
1315
+ options.retryOptions,
1316
+ chunkSignal,
1317
+ (retryAttempt, nextRetryDelayMs) => options.onProgress?.({
1318
+ currentChunk: completed,
1319
+ totalChunks: chunks.length,
1320
+ percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
1321
+ chunkIndex: index,
1322
+ originalTextRange: input.originalTextRange,
1323
+ status: "synthesizing",
1324
+ durationMs: Date.now() - startedAt,
1325
+ retryAttempt,
1326
+ nextRetryDelayMs,
1327
+ isRetrying: true
1328
+ })
1329
+ );
1330
+ } catch (error) {
1331
+ if (chunkScope?.timedOut())
1332
+ throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
1333
+ throw error;
1334
+ } finally {
1335
+ chunkScope?.dispose();
1336
+ }
1111
1337
  results[index] = {
1112
1338
  ...result,
1113
1339
  ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
@@ -1162,6 +1388,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1162
1388
  durationMs: Date.now() - startedAt
1163
1389
  });
1164
1390
  } catch (error) {
1391
+ failedIndices.add(index);
1165
1392
  options.onProgress?.({
1166
1393
  currentChunk: completed,
1167
1394
  totalChunks: chunks.length,
@@ -1172,24 +1399,42 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
1172
1399
  durationMs: Date.now() - startedAt,
1173
1400
  error
1174
1401
  });
1175
- throw error;
1402
+ if (options.cancelOnFailure !== false) jobScope?.abort();
1403
+ firstError ?? (firstError = error);
1404
+ return;
1176
1405
  }
1177
1406
  }
1178
1407
  };
1179
1408
  await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
1409
+ if (failedIndices.size > 0) {
1410
+ const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
1411
+ 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]),
1415
+ failedChunkIndices: [...failedIndices],
1416
+ totalChunks: chunks.length
1417
+ };
1418
+ throw error;
1419
+ }
1180
1420
  const orderedResults = results.filter((result) => result !== void 0);
1181
1421
  return {
1182
1422
  ok: true,
1183
1423
  success: true,
1184
1424
  status: "success",
1185
- value: mergeSynthesisResults(orderedResults, {
1425
+ value: await mergeSynthesisResults(orderedResults, {
1186
1426
  format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
1187
- signal: options.signal
1427
+ signal: jobScope?.signal ?? options.signal,
1428
+ customMerger: options.customMerger,
1429
+ outputMimeType: options.outputMimeType,
1430
+ postMergeValidator: options.postMergeValidator
1188
1431
  })
1189
1432
  };
1190
1433
  } catch (error) {
1191
1434
  const synthesisError = toSynthesisError(error);
1192
- return failure(synthesisError);
1435
+ return failure(synthesisError, partialResultFrom(error));
1436
+ } finally {
1437
+ fallbackJobScope?.dispose();
1193
1438
  }
1194
1439
  }
1195
1440
  function withValidationSignal(options, signal) {
@@ -1210,14 +1455,14 @@ var AzureTtsClient = class {
1210
1455
  __privateSet(this, _options, options);
1211
1456
  }
1212
1457
  async synthesize(ssml) {
1213
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
1458
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
1214
1459
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
1215
1460
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
1216
- const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
1461
+ const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts };
1217
1462
  return synthesizeSpeech(ssml, config);
1218
1463
  }
1219
1464
  async synthesizeSsml(ssml, options = {}) {
1220
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
1465
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
1221
1466
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
1222
1467
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
1223
1468
  return synthesizeSsml(ssml, {
@@ -1227,13 +1472,14 @@ var AzureTtsClient = class {
1227
1472
  outputFormat: options.outputFormat ?? outputFormat,
1228
1473
  signal: options.signal ?? signal,
1229
1474
  timeoutMs: options.timeoutMs ?? timeoutMs,
1475
+ timeouts: options.timeouts ?? timeouts,
1230
1476
  sourceNodePath: options.sourceNodePath,
1231
1477
  sourceTextSegments: options.sourceTextSegments,
1232
1478
  sourceMarkers: options.sourceMarkers
1233
1479
  });
1234
1480
  }
1235
1481
  async synthesizeChunks(chunks, options = {}) {
1236
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
1482
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
1237
1483
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
1238
1484
  return synthesizeSsmlChunks(chunks, {
1239
1485
  endpoint,
@@ -1242,10 +1488,17 @@ var AzureTtsClient = class {
1242
1488
  outputFormat: options.outputFormat ?? outputFormat,
1243
1489
  signal: options.signal ?? signal,
1244
1490
  timeoutMs: options.timeoutMs ?? timeoutMs,
1491
+ timeouts: options.timeouts ?? timeouts,
1245
1492
  sourceNodePath: options.sourceNodePath,
1246
1493
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
1247
1494
  concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
1248
- retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
1495
+ retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
1496
+ cancelOnFailure: options.cancelOnFailure,
1497
+ resumeChunks: options.resumeChunks,
1498
+ resumeChunkIndices: options.resumeChunkIndices,
1499
+ customMerger: options.customMerger,
1500
+ outputMimeType: options.outputMimeType,
1501
+ postMergeValidator: options.postMergeValidator
1249
1502
  });
1250
1503
  }
1251
1504
  async synthesizeSsmlSafe(ssml, options = {}) {
@@ -1257,6 +1510,7 @@ var AzureTtsClient = class {
1257
1510
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
1258
1511
  signal: options.signal ?? __privateGet(this, _options).signal,
1259
1512
  timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
1513
+ timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
1260
1514
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
1261
1515
  concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
1262
1516
  retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
@@ -1350,7 +1604,9 @@ async function fetchAzureVoiceCatalog(options) {
1350
1604
  voiceCount: sortedVoices.length,
1351
1605
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1352
1606
  apiVersion: AZURE_VOICE_API_VERSION,
1353
- regions
1607
+ regions,
1608
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
1609
+ regionDiffs: {}
1354
1610
  }
1355
1611
  };
1356
1612
  }
@@ -1359,6 +1615,7 @@ export {
1359
1615
  AzureTtsClient,
1360
1616
  AzureTtsError,
1361
1617
  AzureTtsSdkError,
1618
+ BatchChunkValidationError,
1362
1619
  ChunkValidationError,
1363
1620
  DEFAULT_OUTPUT_FORMAT,
1364
1621
  MergeError,
@@ -1376,6 +1633,7 @@ export {
1376
1633
  fromPlainTextToSsml,
1377
1634
  getAzureVoiceCatalogMetadata,
1378
1635
  getBuiltInVoiceCatalogMetadata,
1636
+ getRetryAfterDelayMs,
1379
1637
  getSsmlSourceMap,
1380
1638
  inspectAudioSpecification,
1381
1639
  isValidAzureAudioDuration,