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.js CHANGED
@@ -41,6 +41,7 @@ __export(src_exports, {
41
41
  AzureTtsClient: () => AzureTtsClient,
42
42
  AzureTtsError: () => AzureTtsError,
43
43
  AzureTtsSdkError: () => AzureTtsSdkError,
44
+ BatchChunkValidationError: () => BatchChunkValidationError,
44
45
  ChunkValidationError: () => ChunkValidationError,
45
46
  DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
46
47
  MergeError: () => MergeError,
@@ -58,6 +59,7 @@ __export(src_exports, {
58
59
  fromPlainTextToSsml: () => fromPlainTextToSsml,
59
60
  getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
60
61
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
62
+ getRetryAfterDelayMs: () => getRetryAfterDelayMs,
61
63
  getSsmlSourceMap: () => getSsmlSourceMap,
62
64
  inspectAudioSpecification: () => inspectAudioSpecification,
63
65
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
@@ -2723,7 +2725,9 @@ var AZURE_VOICE_CATALOG_METADATA = {
2723
2725
  apiVersion: "2025-10-01",
2724
2726
  generatedAt: "2026-08-28T00:00:00.000Z",
2725
2727
  regions: [],
2726
- voiceCount: AZURE_VOICE_DEFINITIONS.length
2728
+ voiceCount: AZURE_VOICE_DEFINITIONS.length,
2729
+ expiresAt: "2026-09-04T00:00:00.000Z",
2730
+ regionDiffs: {}
2727
2731
  };
2728
2732
 
2729
2733
  // packages/ssml-core/src/voiceCatalog.ts
@@ -2737,7 +2741,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2737
2741
 
2738
2742
  // packages/azure-tts-client/src/errors.ts
2739
2743
  var AzureTtsError = class extends Error {
2740
- constructor(status, statusText, responseBody, requestId) {
2744
+ constructor(status, statusText, responseBody, requestId, responseHeaders) {
2741
2745
  super(`Azure TTS request failed: ${status} ${statusText}`);
2742
2746
  this.kind = "azure-api-error";
2743
2747
  this.name = "AzureTtsError";
@@ -2745,8 +2749,37 @@ var AzureTtsError = class extends Error {
2745
2749
  this.statusText = statusText;
2746
2750
  this.responseBody = responseBody;
2747
2751
  this.requestId = requestId;
2752
+ const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
2753
+ const seconds = value ? Number(value.trim()) : NaN;
2754
+ const date = value ? Date.parse(value) : NaN;
2755
+ if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
2756
+ else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
2748
2757
  }
2749
2758
  };
2759
+ function getRetryAfterDelayMs(error) {
2760
+ if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
2761
+ if (!error || typeof error !== "object") return void 0;
2762
+ const candidate = error;
2763
+ if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
2764
+ const headers = candidate.headers ?? candidate.response?.headers;
2765
+ if (headers instanceof Headers) {
2766
+ const value = headers.get("retry-after");
2767
+ if (!value) return void 0;
2768
+ const seconds = Number(value.trim());
2769
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
2770
+ const date = Date.parse(value);
2771
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
2772
+ }
2773
+ if (headers && typeof headers === "object") {
2774
+ const value = headers["retry-after"] ?? headers["Retry-After"];
2775
+ if (typeof value !== "string") return void 0;
2776
+ const seconds = Number(value.trim());
2777
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
2778
+ const date = Date.parse(value);
2779
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
2780
+ }
2781
+ return void 0;
2782
+ }
2750
2783
  var AzureTtsSdkError = class extends AzureTtsError {
2751
2784
  constructor(errorDetails) {
2752
2785
  super(0, "Speech SDK", errorDetails, null);
@@ -4498,7 +4531,9 @@ var AZURE_VOICE_CATALOG_METADATA2 = {
4498
4531
  apiVersion: "2025-10-01",
4499
4532
  generatedAt: "2026-08-28T00:00:00.000Z",
4500
4533
  regions: [],
4501
- voiceCount: AZURE_VOICE_DEFINITIONS2.length
4534
+ voiceCount: AZURE_VOICE_DEFINITIONS2.length,
4535
+ expiresAt: "2026-09-04T00:00:00.000Z",
4536
+ regionDiffs: {}
4502
4537
  };
4503
4538
 
4504
4539
  // packages/azure-tts-client/src/outputFormats.ts
@@ -4630,6 +4665,8 @@ function formatAudioSpecification(format) {
4630
4665
  const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
4631
4666
  const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
4632
4667
  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";
4668
+ const bitDepthMatch = /(\d+)bit/i.exec(format);
4669
+ 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;
4633
4670
  return {
4634
4671
  format,
4635
4672
  mimeType: resolveMimeType(format),
@@ -4637,6 +4674,9 @@ function formatAudioSpecification(format) {
4637
4674
  sampleRate,
4638
4675
  channels,
4639
4676
  ...bitrate ? { bitrate } : {},
4677
+ ...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
4678
+ ...container ? { container } : {},
4679
+ isVbr: /vbr/i.test(format),
4640
4680
  isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
4641
4681
  };
4642
4682
  }
@@ -4672,6 +4712,8 @@ function parseMp3Specification(buffer, format) {
4672
4712
  sampleRate,
4673
4713
  channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
4674
4714
  bitrate: bitrateKbps * 1e3,
4715
+ container: "mp3-raw",
4716
+ isVbr: false,
4675
4717
  isCompressed: true
4676
4718
  };
4677
4719
  }
@@ -4693,6 +4735,9 @@ function inspectAudioSpecification(buffer, format) {
4693
4735
  sampleRate,
4694
4736
  channels,
4695
4737
  ...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
4738
+ bitDepth: bitsPerSample,
4739
+ container: "riff-wave",
4740
+ isVbr: false,
4696
4741
  isCompressed: formatCode !== 1
4697
4742
  };
4698
4743
  }
@@ -4703,7 +4748,7 @@ function validateAudioSpecifications(specs) {
4703
4748
  const first = specs[0];
4704
4749
  if (!first) return;
4705
4750
  const mismatch = specs.find(
4706
- (spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate
4751
+ (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
4707
4752
  );
4708
4753
  if (mismatch)
4709
4754
  throw new AudioFormatMismatchError(
@@ -4906,9 +4951,7 @@ async function synthesizeSsml(ssml, config) {
4906
4951
  };
4907
4952
  }
4908
4953
  if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
4909
- const unmapped = { mappingStatus: "unmapped" };
4910
- Object.defineProperty(unmapped, "mappingStatus", { value: "unmapped", enumerable: false });
4911
- return unmapped;
4954
+ return { mappingStatus: "unmapped" };
4912
4955
  }
4913
4956
  const value = text ?? "";
4914
4957
  let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
@@ -4990,8 +5033,13 @@ async function synthesizeSsml(ssml, config) {
4990
5033
  ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
4991
5034
  ...requestId ? { requestId } : {}
4992
5035
  };
4993
- if (event.mappingStatus === "unmapped")
5036
+ if (event.mappingStatus === "unmapped") {
4994
5037
  Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
5038
+ Object.defineProperty(mapped, "toJSON", {
5039
+ value: () => ({ ...mapped, mappingStatus: "unmapped" }),
5040
+ enumerable: false
5041
+ });
5042
+ }
4995
5043
  return mapped;
4996
5044
  };
4997
5045
  const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
@@ -5014,10 +5062,11 @@ async function synthesizeSsml(ssml, config) {
5014
5062
  abortHandler = () => rejectWithError(new SynthesisCancelledError());
5015
5063
  config.signal.addEventListener("abort", abortHandler, { once: true });
5016
5064
  }
5017
- if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
5065
+ const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
5066
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
5018
5067
  timeout = setTimeout(
5019
- () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
5020
- config.timeoutMs
5068
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
5069
+ timeoutMs
5021
5070
  );
5022
5071
  }
5023
5072
  synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
@@ -5036,7 +5085,9 @@ function isRetryableSynthesisError(error) {
5036
5085
  if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
5037
5086
  return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
5038
5087
  }
5039
- function retryDelay(options, retryAttempt) {
5088
+ function retryDelay(options, retryAttempt, error) {
5089
+ const retryAfterMs = getRetryAfterDelayMs(error);
5090
+ if (retryAfterMs !== void 0) return retryAfterMs;
5040
5091
  const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
5041
5092
  return Math.floor(Math.random() * (base + 1));
5042
5093
  }
@@ -5068,7 +5119,8 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
5068
5119
  const options = retryOptions ? {
5069
5120
  maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
5070
5121
  initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
5071
- maxDelayMs: Math.max(0, retryOptions.maxDelayMs)
5122
+ maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
5123
+ shouldRetry: retryOptions.shouldRetry
5072
5124
  } : void 0;
5073
5125
  let attempt = 0;
5074
5126
  while (true) {
@@ -5076,17 +5128,56 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
5076
5128
  try {
5077
5129
  return await synthesizeSsml(ssml, config);
5078
5130
  } catch (error) {
5079
- if (!options || attempt >= options.maxRetries || !isRetryableSynthesisError(error)) throw error;
5131
+ if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
5132
+ throw error;
5080
5133
  attempt += 1;
5081
- const delayMs = retryDelay(options, attempt);
5134
+ const delayMs = retryDelay(options, attempt, error);
5082
5135
  onRetry(attempt, delayMs);
5083
5136
  await waitForRetry(delayMs, config.signal);
5084
5137
  }
5085
5138
  }
5086
5139
  }
5140
+ function createAbortScope(parent, timeoutMs) {
5141
+ const controller = new AbortController();
5142
+ let didTimeout = false;
5143
+ const onAbort = () => controller.abort();
5144
+ if (parent?.aborted) controller.abort();
5145
+ parent?.addEventListener("abort", onAbort, { once: true });
5146
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
5147
+ didTimeout = true;
5148
+ controller.abort();
5149
+ }, timeoutMs) : void 0;
5150
+ return {
5151
+ signal: controller.signal,
5152
+ timedOut: () => didTimeout,
5153
+ dispose: () => {
5154
+ if (timer) clearTimeout(timer);
5155
+ parent?.removeEventListener("abort", onAbort);
5156
+ },
5157
+ abort: () => controller.abort()
5158
+ };
5159
+ }
5160
+ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
5161
+ const scope = createAbortScope(config.signal, timeoutMs);
5162
+ try {
5163
+ return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
5164
+ } catch (error) {
5165
+ if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
5166
+ throw error;
5167
+ } finally {
5168
+ scope.dispose();
5169
+ }
5170
+ }
5087
5171
  async function synthesizeSsmlChunks(chunks, config) {
5088
5172
  const results = new Array(chunks.length);
5089
5173
  const totalChunks = chunks.length;
5174
+ const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
5175
+ for (const [index, cached] of cachedChunks) {
5176
+ if (index >= 0 && index < totalChunks) results[index] = cached;
5177
+ }
5178
+ const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
5179
+ const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
5180
+ const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
5090
5181
  const report = (event) => config.onProgress?.(event);
5091
5182
  for (const [index, chunk] of chunks.entries()) {
5092
5183
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
@@ -5100,13 +5191,17 @@ async function synthesizeSsmlChunks(chunks, config) {
5100
5191
  durationMs: 0
5101
5192
  });
5102
5193
  }
5103
- let completed = 0;
5194
+ let completed = [...results].filter((result) => result !== void 0).length;
5104
5195
  let nextIndex = 0;
5105
5196
  const concurrency = resolveConcurrency(config.concurrency, chunks.length);
5197
+ let firstError;
5198
+ const failedIndices = /* @__PURE__ */ new Set();
5106
5199
  const worker = async () => {
5107
5200
  while (true) {
5108
5201
  const index = nextIndex++;
5109
5202
  if (index >= chunks.length) return;
5203
+ if (!shouldSynthesize(index)) continue;
5204
+ if (firstError && config.cancelOnFailure !== false) return;
5110
5205
  const chunk = chunks[index];
5111
5206
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
5112
5207
  report({
@@ -5120,10 +5215,11 @@ async function synthesizeSsmlChunks(chunks, config) {
5120
5215
  });
5121
5216
  const startedAt = Date.now();
5122
5217
  try {
5123
- const result = await synthesizeWithRetry(
5218
+ const result = await synthesizeChunkWithTimeout(
5124
5219
  input.ssml,
5125
5220
  {
5126
5221
  ...config,
5222
+ signal: scope.signal,
5127
5223
  ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
5128
5224
  ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
5129
5225
  ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
@@ -5132,6 +5228,7 @@ async function synthesizeSsmlChunks(chunks, config) {
5132
5228
  onProgress: void 0
5133
5229
  },
5134
5230
  config.retryOptions,
5231
+ config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
5135
5232
  (retryAttempt, nextRetryDelayMs) => report({
5136
5233
  currentChunk: completed,
5137
5234
  totalChunks,
@@ -5157,6 +5254,7 @@ async function synthesizeSsmlChunks(chunks, config) {
5157
5254
  durationMs: Date.now() - startedAt
5158
5255
  });
5159
5256
  } catch (error) {
5257
+ failedIndices.add(index);
5160
5258
  report({
5161
5259
  currentChunk: completed,
5162
5260
  totalChunks,
@@ -5167,16 +5265,36 @@ async function synthesizeSsmlChunks(chunks, config) {
5167
5265
  durationMs: Date.now() - startedAt,
5168
5266
  error
5169
5267
  });
5170
- throw error;
5268
+ firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
5269
+ if (config.cancelOnFailure !== false) scope.abort();
5270
+ return;
5171
5271
  }
5172
5272
  }
5173
5273
  };
5174
- await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
5175
- const orderedResults = results.filter((result) => result !== void 0);
5176
- return mergeSynthesisResults(orderedResults, {
5177
- format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
5178
- signal: config.signal
5179
- });
5274
+ try {
5275
+ await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
5276
+ if (firstError) throw firstError;
5277
+ const orderedResults = results.filter((result) => result !== void 0);
5278
+ return await mergeSynthesisResults(orderedResults, {
5279
+ format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
5280
+ signal: scope.signal,
5281
+ customMerger: config.customMerger,
5282
+ outputMimeType: config.outputMimeType,
5283
+ postMergeValidator: config.postMergeValidator
5284
+ });
5285
+ } catch (error) {
5286
+ const partial = {
5287
+ synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
5288
+ completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
5289
+ pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
5290
+ failedChunkIndices: [...failedIndices],
5291
+ totalChunks
5292
+ };
5293
+ if (error && typeof error === "object") error.partialResult = partial;
5294
+ throw error;
5295
+ } finally {
5296
+ scope.dispose();
5297
+ }
5180
5298
  }
5181
5299
  function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
5182
5300
  const boundaries = [];
@@ -5270,13 +5388,24 @@ function mergeSynthesisResults(results, options) {
5270
5388
  if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
5271
5389
  throw new MergeError("The custom audio merger returned an invalid audio buffer.");
5272
5390
  if (signal.aborted) throw new SynthesisCancelledError();
5273
- return createMergedResult(
5391
+ const result = createMergedResult(
5274
5392
  results,
5275
5393
  merged,
5276
5394
  format,
5277
5395
  inspectAudioSpecification(merged, format),
5278
5396
  resolvedOptions.outputMimeType
5279
5397
  );
5398
+ return Promise.resolve(
5399
+ resolvedOptions.postMergeValidator?.(result, {
5400
+ format,
5401
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
5402
+ inputSpecs,
5403
+ signal
5404
+ })
5405
+ ).then((valid) => {
5406
+ if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
5407
+ return result;
5408
+ });
5280
5409
  }).catch((error) => {
5281
5410
  if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5282
5411
  throw error;
@@ -5284,13 +5413,28 @@ function mergeSynthesisResults(results, options) {
5284
5413
  });
5285
5414
  }
5286
5415
  try {
5287
- return createMergedResult(
5416
+ const result = createMergedResult(
5288
5417
  results,
5289
5418
  mergeAudioBuffers(buffers, { format }),
5290
5419
  format,
5291
5420
  inputSpecs[0],
5292
5421
  resolvedOptions.outputMimeType
5293
5422
  );
5423
+ if (resolvedOptions.postMergeValidator) {
5424
+ const validation = resolvedOptions.postMergeValidator(result, {
5425
+ format,
5426
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
5427
+ inputSpecs,
5428
+ signal
5429
+ });
5430
+ if (validation instanceof Promise)
5431
+ return validation.then((valid) => {
5432
+ if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
5433
+ return result;
5434
+ });
5435
+ if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
5436
+ }
5437
+ return result;
5294
5438
  } catch (error) {
5295
5439
  if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5296
5440
  throw error;
@@ -5311,8 +5455,52 @@ var ChunkValidationError = class extends Error {
5311
5455
  this.diagnostics = diagnostics;
5312
5456
  }
5313
5457
  };
5314
- function failure(error) {
5315
- return { ok: false, success: false, status: error.kind, error };
5458
+ var BatchChunkValidationError = class extends ChunkValidationError {
5459
+ constructor(chunkDiagnostics) {
5460
+ const first = chunkDiagnostics[0];
5461
+ super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
5462
+ this.name = "BatchChunkValidationError";
5463
+ this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
5464
+ this.chunkDiagnostics = chunkDiagnostics;
5465
+ this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
5466
+ this.errorCount = this.totalErrorCount;
5467
+ this.totalErrors = this.totalErrorCount;
5468
+ }
5469
+ };
5470
+ function failure(error, partialResult) {
5471
+ return {
5472
+ ok: false,
5473
+ success: false,
5474
+ status: error.kind,
5475
+ error,
5476
+ ...partialResult ? { partialResult } : {}
5477
+ };
5478
+ }
5479
+ function partialResultFrom(error) {
5480
+ if (!error || typeof error !== "object") return void 0;
5481
+ const partial = error.partialResult;
5482
+ if (!partial || typeof partial !== "object") return void 0;
5483
+ return partial;
5484
+ }
5485
+ function createSafeAbortScope(parent, timeoutMs) {
5486
+ const controller = new AbortController();
5487
+ let didTimeout = false;
5488
+ const onAbort = () => controller.abort();
5489
+ if (parent?.aborted) controller.abort();
5490
+ parent?.addEventListener("abort", onAbort, { once: true });
5491
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
5492
+ didTimeout = true;
5493
+ controller.abort();
5494
+ }, timeoutMs) : void 0;
5495
+ return {
5496
+ signal: controller.signal,
5497
+ timedOut: () => didTimeout,
5498
+ dispose: () => {
5499
+ if (timer) clearTimeout(timer);
5500
+ parent?.removeEventListener("abort", onAbort);
5501
+ },
5502
+ abort: () => controller.abort()
5503
+ };
5316
5504
  }
5317
5505
  function isRetryable(error) {
5318
5506
  if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
@@ -5327,6 +5515,9 @@ function delayForRetry(options, attempt) {
5327
5515
  const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
5328
5516
  return Math.floor(Math.random() * (base + 1));
5329
5517
  }
5518
+ function retryDelayForError(options, attempt, error) {
5519
+ return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
5520
+ }
5330
5521
  function resolveConcurrency2(value, total) {
5331
5522
  if (value === void 0) return 1;
5332
5523
  if (value === Infinity) return Math.max(1, total);
@@ -5336,7 +5527,8 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
5336
5527
  const retry = options ? {
5337
5528
  maxRetries: Math.max(0, Math.floor(options.maxRetries)),
5338
5529
  initialDelayMs: options.initialDelayMs,
5339
- maxDelayMs: options.maxDelayMs
5530
+ maxDelayMs: options.maxDelayMs,
5531
+ shouldRetry: options.shouldRetry
5340
5532
  } : void 0;
5341
5533
  let attempt = 0;
5342
5534
  while (true) {
@@ -5344,9 +5536,10 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
5344
5536
  try {
5345
5537
  return await synthesize();
5346
5538
  } catch (error) {
5347
- if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
5539
+ if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
5540
+ throw error;
5348
5541
  attempt += 1;
5349
- const delayMs = delayForRetry(retry, attempt);
5542
+ const delayMs = retryDelayForError(retry, attempt, error);
5350
5543
  onRetry(attempt, delayMs);
5351
5544
  if (delayMs > 0)
5352
5545
  await new Promise((resolve, reject) => {
@@ -5370,7 +5563,7 @@ function sharedValidationOptions(options, signal) {
5370
5563
  const runner = createAzureUrlValidatorRunner2(validator, {
5371
5564
  ...options.urlValidation ?? {},
5372
5565
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
5373
- ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
5566
+ ...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
5374
5567
  ...signal ? { signal } : {},
5375
5568
  ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
5376
5569
  });
@@ -5399,7 +5592,11 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
5399
5592
  ok: true,
5400
5593
  success: true,
5401
5594
  status: "success",
5402
- value: await client.synthesizeSsml(ssml, { signal: options.signal })
5595
+ value: await client.synthesizeSsml(ssml, {
5596
+ signal: options.signal,
5597
+ timeoutMs: options.timeouts?.perChunkMs,
5598
+ timeouts: options.timeouts
5599
+ })
5403
5600
  };
5404
5601
  } catch (error) {
5405
5602
  const synthesisError = toSynthesisError(error);
@@ -5407,7 +5604,10 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
5407
5604
  }
5408
5605
  }
5409
5606
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5410
- const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
5607
+ const validationOptions = sharedValidationOptions(
5608
+ { ...options.validation ?? options, timeouts: options.timeouts },
5609
+ options.signal
5610
+ );
5411
5611
  if (options.signal?.aborted) {
5412
5612
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5413
5613
  return failure(error);
@@ -5441,16 +5641,17 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5441
5641
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
5442
5642
  })
5443
5643
  );
5444
- const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
5445
5644
  if (options.signal?.aborted) {
5446
5645
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5447
5646
  return failure(error);
5448
5647
  }
5449
- if (firstInvalidIndex >= 0) {
5450
- const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
5451
- pending(firstInvalidIndex, "failed", error);
5648
+ const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
5649
+ if (chunkDiagnostics.length > 0) {
5650
+ const error = new BatchChunkValidationError(chunkDiagnostics);
5651
+ for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
5452
5652
  return failure(error);
5453
5653
  }
5654
+ let fallbackJobScope;
5454
5655
  try {
5455
5656
  if (client.synthesizeChunks) {
5456
5657
  const normalizedChunks = chunks.map((chunk) => {
@@ -5462,20 +5663,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5462
5663
  outputFormat: options.outputFormat,
5463
5664
  signal: options.signal,
5464
5665
  timeoutMs: options.timeoutMs,
5666
+ timeouts: options.timeouts,
5465
5667
  sourceNodePath: options.sourceNodePath,
5466
5668
  concurrency: options.concurrency,
5467
- retryOptions: options.retryOptions
5669
+ retryOptions: options.retryOptions,
5670
+ cancelOnFailure: options.cancelOnFailure,
5671
+ resumeChunks: options.resumeChunks,
5672
+ resumeChunkIndices: options.resumeChunkIndices,
5673
+ customMerger: options.customMerger,
5674
+ outputMimeType: options.outputMimeType,
5675
+ postMergeValidator: options.postMergeValidator
5468
5676
  });
5469
5677
  return { ok: true, success: true, status: "success", value };
5470
5678
  }
5471
5679
  const results = new Array(chunks.length);
5472
- let completed = 0;
5680
+ const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
5681
+ for (const [index, cached] of cachedChunks) {
5682
+ if (index >= 0 && index < chunks.length) results[index] = cached;
5683
+ }
5684
+ const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
5685
+ const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
5686
+ const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
5687
+ fallbackJobScope = jobScope;
5688
+ const failedIndices = /* @__PURE__ */ new Set();
5689
+ let firstError;
5690
+ let completed = [...results].filter((result) => result !== void 0).length;
5473
5691
  let nextIndex = 0;
5474
5692
  const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
5475
5693
  const worker = async () => {
5476
5694
  while (true) {
5477
5695
  const index = nextIndex++;
5478
5696
  if (index >= chunks.length) return;
5697
+ if (!shouldSynthesize(index)) continue;
5698
+ if (failedIndices.size > 0 && options.cancelOnFailure !== false) return;
5479
5699
  const chunk = chunks[index];
5480
5700
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
5481
5701
  const sourceNodePath = input.sourceNodePath;
@@ -5483,28 +5703,40 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5483
5703
  pending(index, "synthesizing");
5484
5704
  const startedAt = Date.now();
5485
5705
  try {
5486
- const result = await retryableSynthesis(
5487
- () => client.synthesizeSsml(input.ssml, {
5488
- outputFormat: options.outputFormat,
5489
- signal: options.signal,
5490
- timeoutMs: options.timeoutMs,
5491
- sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
5492
- }),
5493
- options.retryOptions,
5494
- options.signal,
5495
- (retryAttempt, nextRetryDelayMs) => options.onProgress?.({
5496
- currentChunk: completed,
5497
- totalChunks: chunks.length,
5498
- percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
5499
- chunkIndex: index,
5500
- originalTextRange: input.originalTextRange,
5501
- status: "synthesizing",
5502
- durationMs: Date.now() - startedAt,
5503
- retryAttempt,
5504
- nextRetryDelayMs,
5505
- isRetrying: true
5506
- })
5507
- );
5706
+ const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
5707
+ const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? options.signal, chunkTimeout ?? options.timeoutMs) : void 0;
5708
+ const chunkSignal = chunkScope?.signal ?? options.signal;
5709
+ let result;
5710
+ try {
5711
+ result = await retryableSynthesis(
5712
+ () => client.synthesizeSsml(input.ssml, {
5713
+ outputFormat: options.outputFormat,
5714
+ signal: chunkSignal,
5715
+ timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
5716
+ sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
5717
+ }),
5718
+ options.retryOptions,
5719
+ chunkSignal,
5720
+ (retryAttempt, nextRetryDelayMs) => options.onProgress?.({
5721
+ currentChunk: completed,
5722
+ totalChunks: chunks.length,
5723
+ percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
5724
+ chunkIndex: index,
5725
+ originalTextRange: input.originalTextRange,
5726
+ status: "synthesizing",
5727
+ durationMs: Date.now() - startedAt,
5728
+ retryAttempt,
5729
+ nextRetryDelayMs,
5730
+ isRetrying: true
5731
+ })
5732
+ );
5733
+ } catch (error) {
5734
+ if (chunkScope?.timedOut())
5735
+ throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
5736
+ throw error;
5737
+ } finally {
5738
+ chunkScope?.dispose();
5739
+ }
5508
5740
  results[index] = {
5509
5741
  ...result,
5510
5742
  ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
@@ -5559,6 +5791,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5559
5791
  durationMs: Date.now() - startedAt
5560
5792
  });
5561
5793
  } catch (error) {
5794
+ failedIndices.add(index);
5562
5795
  options.onProgress?.({
5563
5796
  currentChunk: completed,
5564
5797
  totalChunks: chunks.length,
@@ -5569,24 +5802,42 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5569
5802
  durationMs: Date.now() - startedAt,
5570
5803
  error
5571
5804
  });
5572
- throw error;
5805
+ if (options.cancelOnFailure !== false) jobScope?.abort();
5806
+ firstError ?? (firstError = error);
5807
+ return;
5573
5808
  }
5574
5809
  }
5575
5810
  };
5576
5811
  await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
5812
+ if (failedIndices.size > 0) {
5813
+ const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
5814
+ error.partialResult = {
5815
+ synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
5816
+ completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
5817
+ pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
5818
+ failedChunkIndices: [...failedIndices],
5819
+ totalChunks: chunks.length
5820
+ };
5821
+ throw error;
5822
+ }
5577
5823
  const orderedResults = results.filter((result) => result !== void 0);
5578
5824
  return {
5579
5825
  ok: true,
5580
5826
  success: true,
5581
5827
  status: "success",
5582
- value: mergeSynthesisResults(orderedResults, {
5828
+ value: await mergeSynthesisResults(orderedResults, {
5583
5829
  format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
5584
- signal: options.signal
5830
+ signal: jobScope?.signal ?? options.signal,
5831
+ customMerger: options.customMerger,
5832
+ outputMimeType: options.outputMimeType,
5833
+ postMergeValidator: options.postMergeValidator
5585
5834
  })
5586
5835
  };
5587
5836
  } catch (error) {
5588
5837
  const synthesisError = toSynthesisError(error);
5589
- return failure(synthesisError);
5838
+ return failure(synthesisError, partialResultFrom(error));
5839
+ } finally {
5840
+ fallbackJobScope?.dispose();
5590
5841
  }
5591
5842
  }
5592
5843
  function withValidationSignal(options, signal) {
@@ -5607,14 +5858,14 @@ var AzureTtsClient = class {
5607
5858
  __privateSet(this, _options, options);
5608
5859
  }
5609
5860
  async synthesize(ssml) {
5610
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5861
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5611
5862
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5612
5863
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
5613
- const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
5864
+ const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts };
5614
5865
  return synthesizeSpeech(ssml, config);
5615
5866
  }
5616
5867
  async synthesizeSsml(ssml, options = {}) {
5617
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5868
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5618
5869
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5619
5870
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
5620
5871
  return synthesizeSsml(ssml, {
@@ -5624,13 +5875,14 @@ var AzureTtsClient = class {
5624
5875
  outputFormat: options.outputFormat ?? outputFormat,
5625
5876
  signal: options.signal ?? signal,
5626
5877
  timeoutMs: options.timeoutMs ?? timeoutMs,
5878
+ timeouts: options.timeouts ?? timeouts,
5627
5879
  sourceNodePath: options.sourceNodePath,
5628
5880
  sourceTextSegments: options.sourceTextSegments,
5629
5881
  sourceMarkers: options.sourceMarkers
5630
5882
  });
5631
5883
  }
5632
5884
  async synthesizeChunks(chunks, options = {}) {
5633
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5885
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5634
5886
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5635
5887
  return synthesizeSsmlChunks(chunks, {
5636
5888
  endpoint,
@@ -5639,10 +5891,17 @@ var AzureTtsClient = class {
5639
5891
  outputFormat: options.outputFormat ?? outputFormat,
5640
5892
  signal: options.signal ?? signal,
5641
5893
  timeoutMs: options.timeoutMs ?? timeoutMs,
5894
+ timeouts: options.timeouts ?? timeouts,
5642
5895
  sourceNodePath: options.sourceNodePath,
5643
5896
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5644
5897
  concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
5645
- retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
5898
+ retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
5899
+ cancelOnFailure: options.cancelOnFailure,
5900
+ resumeChunks: options.resumeChunks,
5901
+ resumeChunkIndices: options.resumeChunkIndices,
5902
+ customMerger: options.customMerger,
5903
+ outputMimeType: options.outputMimeType,
5904
+ postMergeValidator: options.postMergeValidator
5646
5905
  });
5647
5906
  }
5648
5907
  async synthesizeSsmlSafe(ssml, options = {}) {
@@ -5654,6 +5913,7 @@ var AzureTtsClient = class {
5654
5913
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
5655
5914
  signal: options.signal ?? __privateGet(this, _options).signal,
5656
5915
  timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
5916
+ timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
5657
5917
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5658
5918
  concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
5659
5919
  retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
@@ -5747,7 +6007,9 @@ async function fetchAzureVoiceCatalog(options) {
5747
6007
  voiceCount: sortedVoices.length,
5748
6008
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5749
6009
  apiVersion: AZURE_VOICE_API_VERSION,
5750
- regions
6010
+ regions,
6011
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
6012
+ regionDiffs: {}
5751
6013
  }
5752
6014
  };
5753
6015
  }
@@ -5757,6 +6019,7 @@ async function fetchAzureVoiceCatalog(options) {
5757
6019
  AzureTtsClient,
5758
6020
  AzureTtsError,
5759
6021
  AzureTtsSdkError,
6022
+ BatchChunkValidationError,
5760
6023
  ChunkValidationError,
5761
6024
  DEFAULT_OUTPUT_FORMAT,
5762
6025
  MergeError,
@@ -5774,6 +6037,7 @@ async function fetchAzureVoiceCatalog(options) {
5774
6037
  fromPlainTextToSsml,
5775
6038
  getAzureVoiceCatalogMetadata,
5776
6039
  getBuiltInVoiceCatalogMetadata,
6040
+ getRetryAfterDelayMs,
5777
6041
  getSsmlSourceMap,
5778
6042
  inspectAudioSpecification,
5779
6043
  isValidAzureAudioDuration,