ssml-builder-js 2.15.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
@@ -37,9 +37,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
37
37
  // src/index.ts
38
38
  var src_exports = {};
39
39
  __export(src_exports, {
40
+ AudioFormatMismatchError: () => AudioFormatMismatchError,
40
41
  AzureTtsClient: () => AzureTtsClient,
41
42
  AzureTtsError: () => AzureTtsError,
42
43
  AzureTtsSdkError: () => AzureTtsSdkError,
44
+ BatchChunkValidationError: () => BatchChunkValidationError,
43
45
  ChunkValidationError: () => ChunkValidationError,
44
46
  DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
45
47
  MergeError: () => MergeError,
@@ -57,7 +59,9 @@ __export(src_exports, {
57
59
  fromPlainTextToSsml: () => fromPlainTextToSsml,
58
60
  getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
59
61
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
62
+ getRetryAfterDelayMs: () => getRetryAfterDelayMs,
60
63
  getSsmlSourceMap: () => getSsmlSourceMap,
64
+ inspectAudioSpecification: () => inspectAudioSpecification,
61
65
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
62
66
  mapSsmlTextNodes: () => mapSsmlTextNodes,
63
67
  mergeAudioBuffers: () => mergeAudioBuffers,
@@ -73,6 +77,7 @@ __export(src_exports, {
73
77
  synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
74
78
  synthesizeSsmlSafe: () => synthesizeSsmlSafe,
75
79
  validateAzureSsml: () => validateAzureSsml,
80
+ validateAzureSsmlChunks: () => validateAzureSsmlChunks,
76
81
  validateSsml: () => validateSsml,
77
82
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
78
83
  });
@@ -2089,6 +2094,7 @@ function tokenizeElements(source) {
2089
2094
  if (parent) parent.childElementCount += 1;
2090
2095
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2091
2096
  const tokenName = nameMatch[1];
2097
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
2092
2098
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2093
2099
  tokens.push({
2094
2100
  attributes,
@@ -2099,12 +2105,14 @@ function tokenizeElements(source) {
2099
2105
  parentName: parent?.name,
2100
2106
  parentVoiceName,
2101
2107
  selfClosing,
2102
- start
2108
+ start,
2109
+ path
2103
2110
  });
2104
2111
  if (!selfClosing) {
2105
2112
  openElements.push({
2106
2113
  childElementCount: 0,
2107
2114
  name: tokenName,
2115
+ path,
2108
2116
  voiceName: tokenVoiceName
2109
2117
  });
2110
2118
  }
@@ -2117,13 +2125,14 @@ function location(source, offset) {
2117
2125
  const line = before.split("\n").length;
2118
2126
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2119
2127
  }
2120
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
2128
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
2121
2129
  diagnostics.push({
2122
2130
  ...location(source, offset),
2123
2131
  message,
2124
2132
  severity,
2125
2133
  source: "ssml-static-validator",
2126
- ...code ? { code } : {}
2134
+ ...code ? { code } : {},
2135
+ ...metadata
2127
2136
  });
2128
2137
  }
2129
2138
  function isSupportedProsodyRate(value) {
@@ -2596,9 +2605,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
2596
2605
  for (const token of tokens) {
2597
2606
  const tokenName = token.name.toLowerCase();
2598
2607
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2608
+ const tokenDiagnosticStart = diagnostics.length;
2599
2609
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2600
2610
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2601
2611
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2612
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
2602
2613
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2603
2614
  addDiagnostic(
2604
2615
  diagnostics,
@@ -2620,12 +2631,30 @@ function urlAttributes(token) {
2620
2631
  return value === void 0 ? [] : [{ attribute, value }];
2621
2632
  });
2622
2633
  }
2634
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
2635
+ const attributes = [...token.attributes.keys()];
2636
+ for (const diagnostic of diagnostics.slice(startIndex)) {
2637
+ const attributeName = attributes.find(
2638
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
2639
+ );
2640
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
2641
+ Object.assign(diagnostic, {
2642
+ range: { start: token.start, end: token.end + 1 },
2643
+ tagName: token.name,
2644
+ ...attributeName ? { attributeName } : {},
2645
+ ...voiceName ? { voiceName } : {},
2646
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
2647
+ nodePath,
2648
+ targetNodePath: [...token.path]
2649
+ });
2650
+ }
2651
+ }
2623
2652
  function validateAzureSsml(ssml, options = {}) {
2624
2653
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2625
2654
  const validator = options.urlValidator ?? options.customUrlValidator;
2626
2655
  if (!validator || typeof ssml !== "string") return diagnostics;
2627
2656
  const runnerOptions = options.urlValidation ?? {};
2628
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
2657
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2629
2658
  ...runnerOptions,
2630
2659
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2631
2660
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -2650,33 +2679,55 @@ function validateAzureSsml(ssml, options = {}) {
2650
2679
  const valid = typeof result === "boolean" ? result : result.valid;
2651
2680
  if (!valid) {
2652
2681
  const reason = typeof result === "boolean" ? void 0 : result.reason;
2682
+ const diagnosticStart = diagnostics.length;
2653
2683
  addDiagnostic(
2654
2684
  diagnostics,
2655
2685
  ssml,
2656
2686
  token.start,
2657
2687
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2658
2688
  );
2689
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2659
2690
  }
2660
2691
  } catch (error) {
2661
2692
  const reason = error instanceof Error ? error.message : String(error);
2693
+ const diagnosticStart = diagnostics.length;
2662
2694
  addDiagnostic(
2663
2695
  diagnostics,
2664
2696
  ssml,
2665
2697
  token.start,
2666
2698
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2667
2699
  );
2700
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2668
2701
  }
2669
2702
  })
2670
2703
  );
2671
2704
  return Promise.all(checks).then(() => diagnostics);
2672
2705
  }
2706
+ async function validateAzureSsmlChunks(chunks, options = {}) {
2707
+ const validator = options.urlValidator ?? options.customUrlValidator;
2708
+ const sharedOptions = validator ? {
2709
+ ...options,
2710
+ urlValidatorRunner: options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2711
+ ...options.urlValidation ?? {},
2712
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2713
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2714
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2715
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2716
+ })
2717
+ } : options;
2718
+ return Promise.all(
2719
+ chunks.map((chunk, chunkIndex) => Promise.resolve(validateAzureSsml(chunk, { ...sharedOptions, chunkIndex })))
2720
+ );
2721
+ }
2673
2722
 
2674
2723
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2675
2724
  var AZURE_VOICE_CATALOG_METADATA = {
2676
2725
  apiVersion: "2025-10-01",
2677
2726
  generatedAt: "2026-08-28T00:00:00.000Z",
2678
2727
  regions: [],
2679
- voiceCount: AZURE_VOICE_DEFINITIONS.length
2728
+ voiceCount: AZURE_VOICE_DEFINITIONS.length,
2729
+ expiresAt: "2026-09-04T00:00:00.000Z",
2730
+ regionDiffs: {}
2680
2731
  };
2681
2732
 
2682
2733
  // packages/ssml-core/src/voiceCatalog.ts
@@ -2690,7 +2741,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2690
2741
 
2691
2742
  // packages/azure-tts-client/src/errors.ts
2692
2743
  var AzureTtsError = class extends Error {
2693
- constructor(status, statusText, responseBody, requestId) {
2744
+ constructor(status, statusText, responseBody, requestId, responseHeaders) {
2694
2745
  super(`Azure TTS request failed: ${status} ${statusText}`);
2695
2746
  this.kind = "azure-api-error";
2696
2747
  this.name = "AzureTtsError";
@@ -2698,8 +2749,37 @@ var AzureTtsError = class extends Error {
2698
2749
  this.statusText = statusText;
2699
2750
  this.responseBody = responseBody;
2700
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());
2701
2757
  }
2702
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
+ }
2703
2783
  var AzureTtsSdkError = class extends AzureTtsError {
2704
2784
  constructor(errorDetails) {
2705
2785
  super(0, "Speech SDK", errorDetails, null);
@@ -2730,6 +2810,14 @@ var MergeError = class extends Error {
2730
2810
  this.cause = cause;
2731
2811
  }
2732
2812
  };
2813
+ var AudioFormatMismatchError = class extends Error {
2814
+ constructor(message, inputSpecs = []) {
2815
+ super(message);
2816
+ this.kind = "audio-format-mismatch";
2817
+ this.name = "AudioFormatMismatchError";
2818
+ this.inputSpecs = inputSpecs;
2819
+ }
2820
+ };
2733
2821
  var UnsupportedMergeFormatError = class extends Error {
2734
2822
  constructor(format) {
2735
2823
  super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
@@ -2739,7 +2827,7 @@ var UnsupportedMergeFormatError = class extends Error {
2739
2827
  }
2740
2828
  };
2741
2829
  function toSynthesisError(error) {
2742
- if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
2830
+ if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
2743
2831
  return error;
2744
2832
  const message = error instanceof Error ? error.message : String(error);
2745
2833
  if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
@@ -3840,6 +3928,7 @@ function tokenizeElements2(source) {
3840
3928
  if (parent) parent.childElementCount += 1;
3841
3929
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
3842
3930
  const tokenName = nameMatch[1];
3931
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
3843
3932
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
3844
3933
  tokens.push({
3845
3934
  attributes,
@@ -3850,12 +3939,14 @@ function tokenizeElements2(source) {
3850
3939
  parentName: parent?.name,
3851
3940
  parentVoiceName,
3852
3941
  selfClosing,
3853
- start
3942
+ start,
3943
+ path
3854
3944
  });
3855
3945
  if (!selfClosing) {
3856
3946
  openElements.push({
3857
3947
  childElementCount: 0,
3858
3948
  name: tokenName,
3949
+ path,
3859
3950
  voiceName: tokenVoiceName
3860
3951
  });
3861
3952
  }
@@ -3868,13 +3959,14 @@ function location2(source, offset) {
3868
3959
  const line = before.split("\n").length;
3869
3960
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
3870
3961
  }
3871
- function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code) {
3962
+ function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
3872
3963
  diagnostics.push({
3873
3964
  ...location2(source, offset),
3874
3965
  message,
3875
3966
  severity,
3876
3967
  source: "ssml-static-validator",
3877
- ...code ? { code } : {}
3968
+ ...code ? { code } : {},
3969
+ ...metadata
3878
3970
  });
3879
3971
  }
3880
3972
  function isSupportedProsodyRate2(value) {
@@ -4337,9 +4429,11 @@ function validateAzureSsmlStatic2(ssml, options = {}) {
4337
4429
  for (const token of tokens) {
4338
4430
  const tokenName = token.name.toLowerCase();
4339
4431
  const tokenVoiceName = tokenName === "voice" ? attr2(token, "name")?.trim() : tokenName === "mstts:turn" ? attr2(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
4432
+ const tokenDiagnosticStart = diagnostics.length;
4340
4433
  validateElement2(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
4341
4434
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
4342
4435
  validateVoiceFeatureMatrix2(token, ssml, diagnostics, tokenVoiceName, definition);
4436
+ annotateTokenDiagnostics2(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
4343
4437
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
4344
4438
  addDiagnostic2(
4345
4439
  diagnostics,
@@ -4361,12 +4455,30 @@ function urlAttributes2(token) {
4361
4455
  return value === void 0 ? [] : [{ attribute, value }];
4362
4456
  });
4363
4457
  }
4458
+ function annotateTokenDiagnostics2(diagnostics, startIndex, token, options, voiceName) {
4459
+ const attributes = [...token.attributes.keys()];
4460
+ for (const diagnostic of diagnostics.slice(startIndex)) {
4461
+ const attributeName = attributes.find(
4462
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
4463
+ );
4464
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
4465
+ Object.assign(diagnostic, {
4466
+ range: { start: token.start, end: token.end + 1 },
4467
+ tagName: token.name,
4468
+ ...attributeName ? { attributeName } : {},
4469
+ ...voiceName ? { voiceName } : {},
4470
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
4471
+ nodePath,
4472
+ targetNodePath: [...token.path]
4473
+ });
4474
+ }
4475
+ }
4364
4476
  function validateAzureSsml2(ssml, options = {}) {
4365
4477
  const diagnostics = validateAzureSsmlStatic2(ssml, options);
4366
4478
  const validator = options.urlValidator ?? options.customUrlValidator;
4367
4479
  if (!validator || typeof ssml !== "string") return diagnostics;
4368
4480
  const runnerOptions = options.urlValidation ?? {};
4369
- const boundedValidator = createAzureUrlValidatorRunner2(validator, {
4481
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner2(validator, {
4370
4482
  ...runnerOptions,
4371
4483
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
4372
4484
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -4391,21 +4503,25 @@ function validateAzureSsml2(ssml, options = {}) {
4391
4503
  const valid = typeof result === "boolean" ? result : result.valid;
4392
4504
  if (!valid) {
4393
4505
  const reason = typeof result === "boolean" ? void 0 : result.reason;
4506
+ const diagnosticStart = diagnostics.length;
4394
4507
  addDiagnostic2(
4395
4508
  diagnostics,
4396
4509
  ssml,
4397
4510
  token.start,
4398
4511
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
4399
4512
  );
4513
+ annotateTokenDiagnostics2(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
4400
4514
  }
4401
4515
  } catch (error) {
4402
4516
  const reason = error instanceof Error ? error.message : String(error);
4517
+ const diagnosticStart = diagnostics.length;
4403
4518
  addDiagnostic2(
4404
4519
  diagnostics,
4405
4520
  ssml,
4406
4521
  token.start,
4407
4522
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
4408
4523
  );
4524
+ annotateTokenDiagnostics2(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
4409
4525
  }
4410
4526
  })
4411
4527
  );
@@ -4415,7 +4531,9 @@ var AZURE_VOICE_CATALOG_METADATA2 = {
4415
4531
  apiVersion: "2025-10-01",
4416
4532
  generatedAt: "2026-08-28T00:00:00.000Z",
4417
4533
  regions: [],
4418
- voiceCount: AZURE_VOICE_DEFINITIONS2.length
4534
+ voiceCount: AZURE_VOICE_DEFINITIONS2.length,
4535
+ expiresAt: "2026-09-04T00:00:00.000Z",
4536
+ regionDiffs: {}
4419
4537
  };
4420
4538
 
4421
4539
  // packages/azure-tts-client/src/outputFormats.ts
@@ -4532,6 +4650,115 @@ function parseWav(buffer) {
4532
4650
  }
4533
4651
  return { chunks, data, format };
4534
4652
  }
4653
+ function formatNumber(format, pattern, fallback) {
4654
+ const match = pattern.exec(format);
4655
+ return match?.[1] ? Number(match[1]) : fallback;
4656
+ }
4657
+ function formatChannels(format, fallback) {
4658
+ if (/stereo|2ch|dual/i.test(format)) return 2;
4659
+ if (/mono|1ch/i.test(format)) return 1;
4660
+ return fallback;
4661
+ }
4662
+ function formatAudioSpecification(format) {
4663
+ const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
4664
+ const channels = formatChannels(format, 0);
4665
+ const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
4666
+ const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
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;
4670
+ return {
4671
+ format,
4672
+ mimeType: resolveMimeType(format),
4673
+ codec,
4674
+ sampleRate,
4675
+ channels,
4676
+ ...bitrate ? { bitrate } : {},
4677
+ ...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
4678
+ ...container ? { container } : {},
4679
+ isVbr: /vbr/i.test(format),
4680
+ isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
4681
+ };
4682
+ }
4683
+ function parseMp3Specification(buffer, format) {
4684
+ const bytes = stripMp3Tags(buffer);
4685
+ const bitrates = [
4686
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
4687
+ [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
4688
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
4689
+ ];
4690
+ const sampleRates = [
4691
+ [44100, 48e3, 32e3],
4692
+ [22050, 24e3, 16e3],
4693
+ [11025, 12e3, 8e3]
4694
+ ];
4695
+ for (let index = 0; index + 4 <= bytes.length; index += 1) {
4696
+ if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
4697
+ const header = bytes[index + 1] ?? 0;
4698
+ const versionBits = header >> 3 & 3;
4699
+ const layer = header >> 1 & 3;
4700
+ const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
4701
+ const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
4702
+ if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
4703
+ const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
4704
+ const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
4705
+ const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
4706
+ const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
4707
+ if (!sampleRate || !bitrateKbps) continue;
4708
+ return {
4709
+ format,
4710
+ mimeType: "audio/mpeg",
4711
+ codec: "mp3",
4712
+ sampleRate,
4713
+ channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
4714
+ bitrate: bitrateKbps * 1e3,
4715
+ container: "mp3-raw",
4716
+ isVbr: false,
4717
+ isCompressed: true
4718
+ };
4719
+ }
4720
+ return void 0;
4721
+ }
4722
+ function inspectAudioSpecification(buffer, format) {
4723
+ if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
4724
+ const parsed = parseWav(buffer);
4725
+ if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
4726
+ const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
4727
+ const sampleRate = view.getUint32(4, true);
4728
+ const channels = view.getUint16(2, true);
4729
+ const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
4730
+ const formatCode = view.getUint16(0, true);
4731
+ return {
4732
+ format,
4733
+ mimeType: "audio/wav",
4734
+ codec: formatCode === 1 ? "pcm" : "unknown",
4735
+ sampleRate,
4736
+ channels,
4737
+ ...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
4738
+ bitDepth: bitsPerSample,
4739
+ container: "riff-wave",
4740
+ isVbr: false,
4741
+ isCompressed: formatCode !== 1
4742
+ };
4743
+ }
4744
+ if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
4745
+ return formatAudioSpecification(format);
4746
+ }
4747
+ function validateAudioSpecifications(specs) {
4748
+ const first = specs[0];
4749
+ if (!first) return;
4750
+ const mismatch = specs.find(
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
4752
+ );
4753
+ if (mismatch)
4754
+ throw new AudioFormatMismatchError(
4755
+ `Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
4756
+ specs
4757
+ );
4758
+ }
4759
+ function isAudioFormatMismatch(error) {
4760
+ return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
4761
+ }
4535
4762
  function writeUint32(target, offset, value) {
4536
4763
  new DataView(target.buffer).setUint32(offset, value, true);
4537
4764
  }
@@ -4617,6 +4844,7 @@ function mergeAudioBuffers(buffers, options) {
4617
4844
  const format = typeof options === "string" ? options : options?.format;
4618
4845
  if (!format) throw new UnsupportedMergeFormatError("");
4619
4846
  try {
4847
+ validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
4620
4848
  if (isWavFormat(format)) return mergeWavBuffers(buffers);
4621
4849
  if (isMp3Format(format)) {
4622
4850
  const parts = buffers.map(stripMp3Tags);
@@ -4639,7 +4867,8 @@ function mergeAudioBuffers(buffers, options) {
4639
4867
  }
4640
4868
  throw new UnsupportedMergeFormatError(format);
4641
4869
  } catch (error) {
4642
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
4870
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
4871
+ throw error;
4643
4872
  throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
4644
4873
  }
4645
4874
  }
@@ -4717,17 +4946,24 @@ async function synthesizeSsml(ssml, config) {
4717
4946
  return {
4718
4947
  originalTextRange: { ...marker.originalTextRange },
4719
4948
  sourceNodePath: [...marker.sourceNodePath],
4720
- textRange: { ...marker.originalTextRange }
4949
+ textRange: { ...marker.originalTextRange },
4950
+ mappingStatus: "exact"
4721
4951
  };
4722
4952
  }
4723
- if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
4953
+ if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
4954
+ return { mappingStatus: "unmapped" };
4955
+ }
4724
4956
  const value = text ?? "";
4725
4957
  let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
4726
- if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
4958
+ let mappingStatus = "exact";
4959
+ if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
4727
4960
  localStart = -1;
4961
+ mappingStatus = "fallback";
4962
+ }
4728
4963
  if (localStart < 0 || localStart > sourceText.length) {
4729
4964
  localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
4730
4965
  if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
4966
+ mappingStatus = "fallback";
4731
4967
  }
4732
4968
  localStart = Math.max(0, localStart);
4733
4969
  const localEnd = Math.min(sourceText.length, localStart + value.length);
@@ -4738,7 +4974,8 @@ async function synthesizeSsml(ssml, config) {
4738
4974
  return {
4739
4975
  originalTextRange: { ...fallbackRange },
4740
4976
  textRange: { ...fallbackRange },
4741
- ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
4977
+ ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
4978
+ mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
4742
4979
  };
4743
4980
  };
4744
4981
  synthesizer.wordBoundary = (_sender, event) => {
@@ -4787,20 +5024,32 @@ async function synthesizeSsml(ssml, config) {
4787
5024
  );
4788
5025
  const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
4789
5026
  const requestId = result.resultId;
4790
- const addSourceMetadata = (event) => ({
4791
- ...event,
4792
- ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
4793
- ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
4794
- ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
4795
- ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
4796
- ...requestId ? { requestId } : {}
4797
- });
5027
+ const addSourceMetadata = (event) => {
5028
+ const mapped = {
5029
+ ...event,
5030
+ ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
5031
+ ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
5032
+ ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
5033
+ ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
5034
+ ...requestId ? { requestId } : {}
5035
+ };
5036
+ if (event.mappingStatus === "unmapped") {
5037
+ Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
5038
+ Object.defineProperty(mapped, "toJSON", {
5039
+ value: () => ({ ...mapped, mappingStatus: "unmapped" }),
5040
+ enumerable: false
5041
+ });
5042
+ }
5043
+ return mapped;
5044
+ };
4798
5045
  const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
4799
5046
  const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
4800
5047
  const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
4801
5048
  resolve({
4802
5049
  audioData: result.audioData,
4803
5050
  durationMs,
5051
+ audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
5052
+ mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
4804
5053
  ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
4805
5054
  ...requestId ? { requestId } : {},
4806
5055
  ...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
@@ -4813,10 +5062,11 @@ async function synthesizeSsml(ssml, config) {
4813
5062
  abortHandler = () => rejectWithError(new SynthesisCancelledError());
4814
5063
  config.signal.addEventListener("abort", abortHandler, { once: true });
4815
5064
  }
4816
- if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
5065
+ const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
5066
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
4817
5067
  timeout = setTimeout(
4818
- () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
4819
- config.timeoutMs
5068
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
5069
+ timeoutMs
4820
5070
  );
4821
5071
  }
4822
5072
  synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
@@ -4825,9 +5075,109 @@ async function synthesizeSsml(ssml, config) {
4825
5075
  }
4826
5076
  });
4827
5077
  }
5078
+ function isRetryableSynthesisError(error) {
5079
+ if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
5080
+ if (error instanceof AzureTtsError && error.status !== 0)
5081
+ return error.status === 429 || error.status >= 500 && error.status < 600;
5082
+ const message = error instanceof Error ? error.message : String(error);
5083
+ if (/\b4\d{2}\b/.test(message)) return false;
5084
+ const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
5085
+ if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
5086
+ return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
5087
+ }
5088
+ function retryDelay(options, retryAttempt, error) {
5089
+ const retryAfterMs = getRetryAfterDelayMs(error);
5090
+ if (retryAfterMs !== void 0) return retryAfterMs;
5091
+ const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
5092
+ return Math.floor(Math.random() * (base + 1));
5093
+ }
5094
+ function resolveConcurrency(value, total) {
5095
+ if (value === void 0) return 1;
5096
+ if (value === Infinity) return Math.max(1, total);
5097
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
5098
+ }
5099
+ async function waitForRetry(delayMs, signal) {
5100
+ if (signal?.aborted) throw new SynthesisCancelledError();
5101
+ if (delayMs <= 0) return;
5102
+ await new Promise((resolve, reject) => {
5103
+ let timer;
5104
+ const abort = () => {
5105
+ clearTimeout(timer);
5106
+ signal?.removeEventListener("abort", abort);
5107
+ reject(new SynthesisCancelledError());
5108
+ };
5109
+ timer = setTimeout(() => {
5110
+ signal?.removeEventListener("abort", abort);
5111
+ resolve();
5112
+ }, delayMs);
5113
+ if (signal) {
5114
+ signal.addEventListener("abort", abort, { once: true });
5115
+ }
5116
+ });
5117
+ }
5118
+ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
5119
+ const options = retryOptions ? {
5120
+ maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
5121
+ initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
5122
+ maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
5123
+ shouldRetry: retryOptions.shouldRetry
5124
+ } : void 0;
5125
+ let attempt = 0;
5126
+ while (true) {
5127
+ if (config.signal?.aborted) throw new SynthesisCancelledError();
5128
+ try {
5129
+ return await synthesizeSsml(ssml, config);
5130
+ } catch (error) {
5131
+ if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
5132
+ throw error;
5133
+ attempt += 1;
5134
+ const delayMs = retryDelay(options, attempt, error);
5135
+ onRetry(attempt, delayMs);
5136
+ await waitForRetry(delayMs, config.signal);
5137
+ }
5138
+ }
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
+ }
4828
5171
  async function synthesizeSsmlChunks(chunks, config) {
4829
- const results = [];
5172
+ const results = new Array(chunks.length);
4830
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);
4831
5181
  const report = (event) => config.onProgress?.(event);
4832
5182
  for (const [index, chunk] of chunks.entries()) {
4833
5183
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
@@ -4841,57 +5191,112 @@ async function synthesizeSsmlChunks(chunks, config) {
4841
5191
  durationMs: 0
4842
5192
  });
4843
5193
  }
4844
- for (const [index, chunk] of chunks.entries()) {
4845
- const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
4846
- report({
4847
- currentChunk: index,
4848
- totalChunks,
4849
- percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
4850
- chunkIndex: index,
4851
- originalTextRange: input.originalTextRange,
4852
- status: "synthesizing",
4853
- durationMs: 0
4854
- });
4855
- const startedAt = Date.now();
4856
- try {
4857
- const result = await synthesizeSsml(input.ssml, {
4858
- ...config,
4859
- ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
4860
- ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
4861
- ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
4862
- ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
4863
- chunkIndex: index,
4864
- onProgress: void 0
4865
- });
4866
- results.push(result);
4867
- report({
4868
- currentChunk: index + 1,
4869
- totalChunks,
4870
- percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
4871
- chunkIndex: index,
4872
- originalTextRange: input.originalTextRange,
4873
- status: "success",
4874
- durationMs: Date.now() - startedAt
4875
- });
4876
- } catch (error) {
5194
+ let completed = [...results].filter((result) => result !== void 0).length;
5195
+ let nextIndex = 0;
5196
+ const concurrency = resolveConcurrency(config.concurrency, chunks.length);
5197
+ let firstError;
5198
+ const failedIndices = /* @__PURE__ */ new Set();
5199
+ const worker = async () => {
5200
+ while (true) {
5201
+ const index = nextIndex++;
5202
+ if (index >= chunks.length) return;
5203
+ if (!shouldSynthesize(index)) continue;
5204
+ if (firstError && config.cancelOnFailure !== false) return;
5205
+ const chunk = chunks[index];
5206
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
4877
5207
  report({
4878
- currentChunk: index,
5208
+ currentChunk: completed,
4879
5209
  totalChunks,
4880
- percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
5210
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
4881
5211
  chunkIndex: index,
4882
5212
  originalTextRange: input.originalTextRange,
4883
- status: "failed",
4884
- durationMs: Date.now() - startedAt,
4885
- error
5213
+ status: "synthesizing",
5214
+ durationMs: 0
4886
5215
  });
4887
- throw error;
5216
+ const startedAt = Date.now();
5217
+ try {
5218
+ const result = await synthesizeChunkWithTimeout(
5219
+ input.ssml,
5220
+ {
5221
+ ...config,
5222
+ signal: scope.signal,
5223
+ ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
5224
+ ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
5225
+ ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
5226
+ ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
5227
+ chunkIndex: index,
5228
+ onProgress: void 0
5229
+ },
5230
+ config.retryOptions,
5231
+ config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
5232
+ (retryAttempt, nextRetryDelayMs) => report({
5233
+ currentChunk: completed,
5234
+ totalChunks,
5235
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5236
+ chunkIndex: index,
5237
+ originalTextRange: input.originalTextRange,
5238
+ status: "synthesizing",
5239
+ durationMs: Date.now() - startedAt,
5240
+ retryAttempt,
5241
+ nextRetryDelayMs,
5242
+ isRetrying: true
5243
+ })
5244
+ );
5245
+ results[index] = result;
5246
+ completed += 1;
5247
+ report({
5248
+ currentChunk: completed,
5249
+ totalChunks,
5250
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5251
+ chunkIndex: index,
5252
+ originalTextRange: input.originalTextRange,
5253
+ status: "success",
5254
+ durationMs: Date.now() - startedAt
5255
+ });
5256
+ } catch (error) {
5257
+ failedIndices.add(index);
5258
+ report({
5259
+ currentChunk: completed,
5260
+ totalChunks,
5261
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5262
+ chunkIndex: index,
5263
+ originalTextRange: input.originalTextRange,
5264
+ status: "failed",
5265
+ durationMs: Date.now() - startedAt,
5266
+ error
5267
+ });
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;
5271
+ }
4888
5272
  }
5273
+ };
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();
4889
5297
  }
4890
- return mergeSynthesisResults(results, {
4891
- format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
4892
- });
4893
5298
  }
4894
- function createMergedResult(results, audioData, format) {
5299
+ function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
4895
5300
  const boundaries = [];
4896
5301
  const visemes = [];
4897
5302
  const bookmarks = [];
@@ -4910,7 +5315,8 @@ function createMergedResult(results, audioData, format) {
4910
5315
  ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
4911
5316
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4912
5317
  ...textRange ? { textRange: { ...textRange } } : {},
4913
- ...requestId ? { requestId } : {}
5318
+ ...requestId ? { requestId } : {},
5319
+ mappingStatus: boundary.mappingStatus ?? "unmapped"
4914
5320
  });
4915
5321
  }
4916
5322
  for (const viseme of result.visemes ?? []) {
@@ -4925,7 +5331,8 @@ function createMergedResult(results, audioData, format) {
4925
5331
  ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
4926
5332
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4927
5333
  ...textRange ? { textRange: { ...textRange } } : {},
4928
- ...requestId ? { requestId } : {}
5334
+ ...requestId ? { requestId } : {},
5335
+ mappingStatus: viseme.mappingStatus ?? "unmapped"
4929
5336
  });
4930
5337
  }
4931
5338
  for (const bookmark of result.bookmarks ?? []) {
@@ -4940,7 +5347,8 @@ function createMergedResult(results, audioData, format) {
4940
5347
  ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
4941
5348
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4942
5349
  ...textRange ? { textRange: { ...textRange } } : {},
4943
- ...requestId ? { requestId } : {}
5350
+ ...requestId ? { requestId } : {},
5351
+ mappingStatus: bookmark.mappingStatus ?? "unmapped"
4944
5352
  });
4945
5353
  }
4946
5354
  durationOffset += Math.max(0, result.durationMs);
@@ -4949,6 +5357,8 @@ function createMergedResult(results, audioData, format) {
4949
5357
  audioData,
4950
5358
  durationMs: durationOffset,
4951
5359
  mimeType: resolveMimeType(format),
5360
+ audioSpec: audioSpec ?? formatAudioSpecification(format),
5361
+ ...outputMimeType ? { mimeType: outputMimeType } : {},
4952
5362
  ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
4953
5363
  ...visemes.length > 0 ? { visemes } : {},
4954
5364
  ...bookmarks.length > 0 ? { bookmarks } : {},
@@ -4961,19 +5371,73 @@ function mergeSynthesisResults(results, options) {
4961
5371
  const format = resolvedOptions?.format;
4962
5372
  if (!format) throw new UnsupportedMergeFormatError("");
4963
5373
  const buffers = results.map((result) => result.audioData);
5374
+ const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
5375
+ validateAudioSpecifications(inputSpecs);
5376
+ const signal = resolvedOptions.signal ?? new AbortController().signal;
5377
+ if (signal.aborted) throw new SynthesisCancelledError();
4964
5378
  if (resolvedOptions.customMerger) {
4965
- return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
5379
+ return Promise.resolve().then(
5380
+ () => resolvedOptions.customMerger?.(buffers, {
5381
+ format,
5382
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
5383
+ inputSpecs,
5384
+ signal
5385
+ })
5386
+ ).then((merged) => {
4966
5387
  if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
4967
- return createMergedResult(results, merged, format);
5388
+ if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
5389
+ throw new MergeError("The custom audio merger returned an invalid audio buffer.");
5390
+ if (signal.aborted) throw new SynthesisCancelledError();
5391
+ const result = createMergedResult(
5392
+ results,
5393
+ merged,
5394
+ format,
5395
+ inspectAudioSpecification(merged, format),
5396
+ resolvedOptions.outputMimeType
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
+ });
4968
5409
  }).catch((error) => {
4969
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
5410
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5411
+ throw error;
4970
5412
  throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
4971
5413
  });
4972
5414
  }
4973
5415
  try {
4974
- return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
5416
+ const result = createMergedResult(
5417
+ results,
5418
+ mergeAudioBuffers(buffers, { format }),
5419
+ format,
5420
+ inputSpecs[0],
5421
+ resolvedOptions.outputMimeType
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;
4975
5438
  } catch (error) {
4976
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
5439
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5440
+ throw error;
4977
5441
  throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
4978
5442
  }
4979
5443
  }
@@ -4991,11 +5455,125 @@ var ChunkValidationError = class extends Error {
4991
5455
  this.diagnostics = diagnostics;
4992
5456
  }
4993
5457
  };
4994
- function failure(error) {
4995
- 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
+ };
5504
+ }
5505
+ function isRetryable(error) {
5506
+ if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
5507
+ const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
5508
+ if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
5509
+ const message = error instanceof Error ? error.message : String(error);
5510
+ if (/\b4\d{2}\b/.test(message)) return false;
5511
+ return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
5512
+ }
5513
+ function delayForRetry(options, attempt) {
5514
+ const maxDelay = Math.max(0, options.maxDelayMs);
5515
+ const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
5516
+ return Math.floor(Math.random() * (base + 1));
5517
+ }
5518
+ function retryDelayForError(options, attempt, error) {
5519
+ return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
5520
+ }
5521
+ function resolveConcurrency2(value, total) {
5522
+ if (value === void 0) return 1;
5523
+ if (value === Infinity) return Math.max(1, total);
5524
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
5525
+ }
5526
+ async function retryableSynthesis(synthesize, options, signal, onRetry) {
5527
+ const retry = options ? {
5528
+ maxRetries: Math.max(0, Math.floor(options.maxRetries)),
5529
+ initialDelayMs: options.initialDelayMs,
5530
+ maxDelayMs: options.maxDelayMs,
5531
+ shouldRetry: options.shouldRetry
5532
+ } : void 0;
5533
+ let attempt = 0;
5534
+ while (true) {
5535
+ if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
5536
+ try {
5537
+ return await synthesize();
5538
+ } catch (error) {
5539
+ if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
5540
+ throw error;
5541
+ attempt += 1;
5542
+ const delayMs = retryDelayForError(retry, attempt, error);
5543
+ onRetry(attempt, delayMs);
5544
+ if (delayMs > 0)
5545
+ await new Promise((resolve, reject) => {
5546
+ const timer = setTimeout(() => {
5547
+ signal?.removeEventListener("abort", abort);
5548
+ resolve();
5549
+ }, delayMs);
5550
+ const abort = () => {
5551
+ clearTimeout(timer);
5552
+ signal?.removeEventListener("abort", abort);
5553
+ reject(new Error("Speech synthesis was cancelled."));
5554
+ };
5555
+ signal?.addEventListener("abort", abort, { once: true });
5556
+ });
5557
+ }
5558
+ }
5559
+ }
5560
+ function sharedValidationOptions(options, signal) {
5561
+ const validator = options.urlValidator ?? options.customUrlValidator;
5562
+ if (!validator) return signal ? withValidationSignal(options, signal) : options;
5563
+ const runner = createAzureUrlValidatorRunner2(validator, {
5564
+ ...options.urlValidation ?? {},
5565
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
5566
+ ...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
5567
+ ...signal ? { signal } : {},
5568
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
5569
+ });
5570
+ return {
5571
+ ...withValidationSignal(options, signal),
5572
+ urlValidatorRunner: runner
5573
+ };
4996
5574
  }
4997
5575
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
4998
- const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
5576
+ const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
4999
5577
  const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
5000
5578
  if (options.signal?.aborted) {
5001
5579
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
@@ -5014,7 +5592,11 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
5014
5592
  ok: true,
5015
5593
  success: true,
5016
5594
  status: "success",
5017
- 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
+ })
5018
5600
  };
5019
5601
  } catch (error) {
5020
5602
  const synthesisError = toSynthesisError(error);
@@ -5022,7 +5604,10 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
5022
5604
  }
5023
5605
  }
5024
5606
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5025
- const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
5607
+ const validationOptions = sharedValidationOptions(
5608
+ { ...options.validation ?? options, timeouts: options.timeouts },
5609
+ options.signal
5610
+ );
5026
5611
  if (options.signal?.aborted) {
5027
5612
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5028
5613
  return failure(error);
@@ -5043,21 +5628,30 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5043
5628
  pending(index, "pending");
5044
5629
  });
5045
5630
  const validations = await Promise.all(
5046
- chunks.map(async (chunk) => {
5631
+ chunks.map(async (chunk, index) => {
5047
5632
  const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
5048
5633
  const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
5049
5634
  const diagnostics = await Promise.resolve(
5050
- validateAzureSsml2(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
5635
+ validateAzureSsml2(ssml, {
5636
+ ...validationOptions,
5637
+ ...sourceNodePath ? { sourceNodePath } : {},
5638
+ chunkIndex: index
5639
+ })
5051
5640
  );
5052
5641
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
5053
5642
  })
5054
5643
  );
5055
- const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
5056
- if (firstInvalidIndex >= 0) {
5057
- const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
5058
- pending(firstInvalidIndex, "failed", error);
5644
+ if (options.signal?.aborted) {
5645
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5646
+ return failure(error);
5647
+ }
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);
5059
5652
  return failure(error);
5060
5653
  }
5654
+ let fallbackJobScope;
5061
5655
  try {
5062
5656
  if (client.synthesizeChunks) {
5063
5657
  const normalizedChunks = chunks.map((chunk) => {
@@ -5069,101 +5663,181 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5069
5663
  outputFormat: options.outputFormat,
5070
5664
  signal: options.signal,
5071
5665
  timeoutMs: options.timeoutMs,
5072
- sourceNodePath: options.sourceNodePath
5666
+ timeouts: options.timeouts,
5667
+ sourceNodePath: options.sourceNodePath,
5668
+ concurrency: options.concurrency,
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
5073
5676
  });
5074
5677
  return { ok: true, success: true, status: "success", value };
5075
5678
  }
5076
- const results = [];
5077
- for (const [index, chunk] of chunks.entries()) {
5078
- const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
5079
- const sourceNodePath = input.sourceNodePath;
5080
- const originalTextRange = input.originalTextRange;
5081
- pending(index, "synthesizing");
5082
- const startedAt = Date.now();
5083
- try {
5084
- const result = await client.synthesizeSsml(input.ssml, {
5085
- outputFormat: options.outputFormat,
5086
- signal: options.signal,
5087
- timeoutMs: options.timeoutMs,
5088
- sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
5089
- });
5090
- results.push({
5091
- ...result,
5092
- ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
5093
- ...sourceNodePath ? {
5094
- boundaries: result.boundaries?.map((event) => ({
5095
- ...event,
5096
- sourceNodePath: [...sourceNodePath],
5097
- ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5098
- })),
5099
- visemes: result.visemes?.map((event) => ({
5100
- ...event,
5101
- sourceNodePath: [...sourceNodePath],
5102
- ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5103
- })),
5104
- bookmarks: result.bookmarks?.map((event) => ({
5105
- ...event,
5106
- sourceNodePath: [...sourceNodePath],
5107
- ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5108
- }))
5109
- } : {},
5110
- ...originalTextRange ? {
5111
- boundaries: result.boundaries?.map((event) => ({
5112
- ...event,
5113
- originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5114
- })),
5115
- wordBoundary: result.wordBoundary?.map((event) => ({
5116
- ...event,
5117
- originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5118
- })),
5119
- wordBoundaries: result.wordBoundaries?.map((event) => ({
5120
- ...event,
5121
- originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5122
- })),
5123
- visemes: result.visemes?.map((event) => ({
5124
- ...event,
5125
- originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5126
- })),
5127
- bookmarks: result.bookmarks?.map((event) => ({
5128
- ...event,
5129
- originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5130
- }))
5131
- } : {}
5132
- });
5133
- options.onProgress?.({
5134
- currentChunk: index + 1,
5135
- totalChunks: chunks.length,
5136
- percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
5137
- chunkIndex: index,
5138
- originalTextRange: input.originalTextRange,
5139
- status: "success",
5140
- durationMs: Date.now() - startedAt
5141
- });
5142
- } catch (error) {
5143
- options.onProgress?.({
5144
- currentChunk: index,
5145
- totalChunks: chunks.length,
5146
- percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
5147
- chunkIndex: index,
5148
- originalTextRange: input.originalTextRange,
5149
- status: "failed",
5150
- durationMs: Date.now() - startedAt,
5151
- error
5152
- });
5153
- throw error;
5679
+ const results = new Array(chunks.length);
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;
5691
+ let nextIndex = 0;
5692
+ const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
5693
+ const worker = async () => {
5694
+ while (true) {
5695
+ const index = nextIndex++;
5696
+ if (index >= chunks.length) return;
5697
+ if (!shouldSynthesize(index)) continue;
5698
+ if (failedIndices.size > 0 && options.cancelOnFailure !== false) return;
5699
+ const chunk = chunks[index];
5700
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
5701
+ const sourceNodePath = input.sourceNodePath;
5702
+ const originalTextRange = input.originalTextRange;
5703
+ pending(index, "synthesizing");
5704
+ const startedAt = Date.now();
5705
+ try {
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
+ }
5740
+ results[index] = {
5741
+ ...result,
5742
+ ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
5743
+ ...sourceNodePath ? {
5744
+ boundaries: result.boundaries?.map((event) => ({
5745
+ ...event,
5746
+ sourceNodePath: [...sourceNodePath],
5747
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5748
+ })),
5749
+ visemes: result.visemes?.map((event) => ({
5750
+ ...event,
5751
+ sourceNodePath: [...sourceNodePath],
5752
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5753
+ })),
5754
+ bookmarks: result.bookmarks?.map((event) => ({
5755
+ ...event,
5756
+ sourceNodePath: [...sourceNodePath],
5757
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5758
+ }))
5759
+ } : {},
5760
+ ...originalTextRange ? {
5761
+ boundaries: result.boundaries?.map((event) => ({
5762
+ ...event,
5763
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5764
+ })),
5765
+ wordBoundary: result.wordBoundary?.map((event) => ({
5766
+ ...event,
5767
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5768
+ })),
5769
+ wordBoundaries: result.wordBoundaries?.map((event) => ({
5770
+ ...event,
5771
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5772
+ })),
5773
+ visemes: result.visemes?.map((event) => ({
5774
+ ...event,
5775
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5776
+ })),
5777
+ bookmarks: result.bookmarks?.map((event) => ({
5778
+ ...event,
5779
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5780
+ }))
5781
+ } : {}
5782
+ };
5783
+ completed += 1;
5784
+ options.onProgress?.({
5785
+ currentChunk: completed,
5786
+ totalChunks: chunks.length,
5787
+ percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
5788
+ chunkIndex: index,
5789
+ originalTextRange: input.originalTextRange,
5790
+ status: "success",
5791
+ durationMs: Date.now() - startedAt
5792
+ });
5793
+ } catch (error) {
5794
+ failedIndices.add(index);
5795
+ options.onProgress?.({
5796
+ currentChunk: completed,
5797
+ totalChunks: chunks.length,
5798
+ percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
5799
+ chunkIndex: index,
5800
+ originalTextRange: input.originalTextRange,
5801
+ status: "failed",
5802
+ durationMs: Date.now() - startedAt,
5803
+ error
5804
+ });
5805
+ if (options.cancelOnFailure !== false) jobScope?.abort();
5806
+ firstError ?? (firstError = error);
5807
+ return;
5808
+ }
5154
5809
  }
5810
+ };
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;
5155
5822
  }
5823
+ const orderedResults = results.filter((result) => result !== void 0);
5156
5824
  return {
5157
5825
  ok: true,
5158
5826
  success: true,
5159
5827
  status: "success",
5160
- value: mergeSynthesisResults(results, {
5161
- format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
5828
+ value: await mergeSynthesisResults(orderedResults, {
5829
+ format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
5830
+ signal: jobScope?.signal ?? options.signal,
5831
+ customMerger: options.customMerger,
5832
+ outputMimeType: options.outputMimeType,
5833
+ postMergeValidator: options.postMergeValidator
5162
5834
  })
5163
5835
  };
5164
5836
  } catch (error) {
5165
5837
  const synthesisError = toSynthesisError(error);
5166
- return failure(synthesisError);
5838
+ return failure(synthesisError, partialResultFrom(error));
5839
+ } finally {
5840
+ fallbackJobScope?.dispose();
5167
5841
  }
5168
5842
  }
5169
5843
  function withValidationSignal(options, signal) {
@@ -5184,14 +5858,14 @@ var AzureTtsClient = class {
5184
5858
  __privateSet(this, _options, options);
5185
5859
  }
5186
5860
  async synthesize(ssml) {
5187
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5861
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5188
5862
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5189
5863
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
5190
- const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
5864
+ const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts };
5191
5865
  return synthesizeSpeech(ssml, config);
5192
5866
  }
5193
5867
  async synthesizeSsml(ssml, options = {}) {
5194
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5868
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5195
5869
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5196
5870
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
5197
5871
  return synthesizeSsml(ssml, {
@@ -5201,13 +5875,14 @@ var AzureTtsClient = class {
5201
5875
  outputFormat: options.outputFormat ?? outputFormat,
5202
5876
  signal: options.signal ?? signal,
5203
5877
  timeoutMs: options.timeoutMs ?? timeoutMs,
5878
+ timeouts: options.timeouts ?? timeouts,
5204
5879
  sourceNodePath: options.sourceNodePath,
5205
5880
  sourceTextSegments: options.sourceTextSegments,
5206
5881
  sourceMarkers: options.sourceMarkers
5207
5882
  });
5208
5883
  }
5209
5884
  async synthesizeChunks(chunks, options = {}) {
5210
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
5885
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
5211
5886
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
5212
5887
  return synthesizeSsmlChunks(chunks, {
5213
5888
  endpoint,
@@ -5216,8 +5891,17 @@ var AzureTtsClient = class {
5216
5891
  outputFormat: options.outputFormat ?? outputFormat,
5217
5892
  signal: options.signal ?? signal,
5218
5893
  timeoutMs: options.timeoutMs ?? timeoutMs,
5894
+ timeouts: options.timeouts ?? timeouts,
5219
5895
  sourceNodePath: options.sourceNodePath,
5220
- onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
5896
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5897
+ concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
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
5221
5905
  });
5222
5906
  }
5223
5907
  async synthesizeSsmlSafe(ssml, options = {}) {
@@ -5229,7 +5913,10 @@ var AzureTtsClient = class {
5229
5913
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
5230
5914
  signal: options.signal ?? __privateGet(this, _options).signal,
5231
5915
  timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
5232
- onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
5916
+ timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
5917
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5918
+ concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
5919
+ retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
5233
5920
  });
5234
5921
  }
5235
5922
  async synthesizeSsmlChunksSafe(chunks, options = {}) {
@@ -5320,15 +6007,19 @@ async function fetchAzureVoiceCatalog(options) {
5320
6007
  voiceCount: sortedVoices.length,
5321
6008
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5322
6009
  apiVersion: AZURE_VOICE_API_VERSION,
5323
- regions
6010
+ regions,
6011
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
6012
+ regionDiffs: {}
5324
6013
  }
5325
6014
  };
5326
6015
  }
5327
6016
  // Annotate the CommonJS export names for ESM import in node:
5328
6017
  0 && (module.exports = {
6018
+ AudioFormatMismatchError,
5329
6019
  AzureTtsClient,
5330
6020
  AzureTtsError,
5331
6021
  AzureTtsSdkError,
6022
+ BatchChunkValidationError,
5332
6023
  ChunkValidationError,
5333
6024
  DEFAULT_OUTPUT_FORMAT,
5334
6025
  MergeError,
@@ -5346,7 +6037,9 @@ async function fetchAzureVoiceCatalog(options) {
5346
6037
  fromPlainTextToSsml,
5347
6038
  getAzureVoiceCatalogMetadata,
5348
6039
  getBuiltInVoiceCatalogMetadata,
6040
+ getRetryAfterDelayMs,
5349
6041
  getSsmlSourceMap,
6042
+ inspectAudioSpecification,
5350
6043
  isValidAzureAudioDuration,
5351
6044
  mapSsmlTextNodes,
5352
6045
  mergeAudioBuffers,
@@ -5362,6 +6055,7 @@ async function fetchAzureVoiceCatalog(options) {
5362
6055
  synthesizeSsmlChunksSafe,
5363
6056
  synthesizeSsmlSafe,
5364
6057
  validateAzureSsml,
6058
+ validateAzureSsmlChunks,
5365
6059
  validateSsml,
5366
6060
  validateSsmlStructureIntegrity
5367
6061
  });