ssml-builder-js 2.15.0 → 2.16.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,6 +37,7 @@ 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,
@@ -58,6 +59,7 @@ __export(src_exports, {
58
59
  getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
59
60
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
60
61
  getSsmlSourceMap: () => getSsmlSourceMap,
62
+ inspectAudioSpecification: () => inspectAudioSpecification,
61
63
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
62
64
  mapSsmlTextNodes: () => mapSsmlTextNodes,
63
65
  mergeAudioBuffers: () => mergeAudioBuffers,
@@ -73,6 +75,7 @@ __export(src_exports, {
73
75
  synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
74
76
  synthesizeSsmlSafe: () => synthesizeSsmlSafe,
75
77
  validateAzureSsml: () => validateAzureSsml,
78
+ validateAzureSsmlChunks: () => validateAzureSsmlChunks,
76
79
  validateSsml: () => validateSsml,
77
80
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
78
81
  });
@@ -2089,6 +2092,7 @@ function tokenizeElements(source) {
2089
2092
  if (parent) parent.childElementCount += 1;
2090
2093
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2091
2094
  const tokenName = nameMatch[1];
2095
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
2092
2096
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2093
2097
  tokens.push({
2094
2098
  attributes,
@@ -2099,12 +2103,14 @@ function tokenizeElements(source) {
2099
2103
  parentName: parent?.name,
2100
2104
  parentVoiceName,
2101
2105
  selfClosing,
2102
- start
2106
+ start,
2107
+ path
2103
2108
  });
2104
2109
  if (!selfClosing) {
2105
2110
  openElements.push({
2106
2111
  childElementCount: 0,
2107
2112
  name: tokenName,
2113
+ path,
2108
2114
  voiceName: tokenVoiceName
2109
2115
  });
2110
2116
  }
@@ -2117,13 +2123,14 @@ function location(source, offset) {
2117
2123
  const line = before.split("\n").length;
2118
2124
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2119
2125
  }
2120
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
2126
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
2121
2127
  diagnostics.push({
2122
2128
  ...location(source, offset),
2123
2129
  message,
2124
2130
  severity,
2125
2131
  source: "ssml-static-validator",
2126
- ...code ? { code } : {}
2132
+ ...code ? { code } : {},
2133
+ ...metadata
2127
2134
  });
2128
2135
  }
2129
2136
  function isSupportedProsodyRate(value) {
@@ -2596,9 +2603,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
2596
2603
  for (const token of tokens) {
2597
2604
  const tokenName = token.name.toLowerCase();
2598
2605
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2606
+ const tokenDiagnosticStart = diagnostics.length;
2599
2607
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2600
2608
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2601
2609
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2610
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
2602
2611
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2603
2612
  addDiagnostic(
2604
2613
  diagnostics,
@@ -2620,12 +2629,30 @@ function urlAttributes(token) {
2620
2629
  return value === void 0 ? [] : [{ attribute, value }];
2621
2630
  });
2622
2631
  }
2632
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
2633
+ const attributes = [...token.attributes.keys()];
2634
+ for (const diagnostic of diagnostics.slice(startIndex)) {
2635
+ const attributeName = attributes.find(
2636
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
2637
+ );
2638
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
2639
+ Object.assign(diagnostic, {
2640
+ range: { start: token.start, end: token.end + 1 },
2641
+ tagName: token.name,
2642
+ ...attributeName ? { attributeName } : {},
2643
+ ...voiceName ? { voiceName } : {},
2644
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
2645
+ nodePath,
2646
+ targetNodePath: [...token.path]
2647
+ });
2648
+ }
2649
+ }
2623
2650
  function validateAzureSsml(ssml, options = {}) {
2624
2651
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2625
2652
  const validator = options.urlValidator ?? options.customUrlValidator;
2626
2653
  if (!validator || typeof ssml !== "string") return diagnostics;
2627
2654
  const runnerOptions = options.urlValidation ?? {};
2628
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
2655
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2629
2656
  ...runnerOptions,
2630
2657
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2631
2658
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -2650,26 +2677,46 @@ function validateAzureSsml(ssml, options = {}) {
2650
2677
  const valid = typeof result === "boolean" ? result : result.valid;
2651
2678
  if (!valid) {
2652
2679
  const reason = typeof result === "boolean" ? void 0 : result.reason;
2680
+ const diagnosticStart = diagnostics.length;
2653
2681
  addDiagnostic(
2654
2682
  diagnostics,
2655
2683
  ssml,
2656
2684
  token.start,
2657
2685
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2658
2686
  );
2687
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2659
2688
  }
2660
2689
  } catch (error) {
2661
2690
  const reason = error instanceof Error ? error.message : String(error);
2691
+ const diagnosticStart = diagnostics.length;
2662
2692
  addDiagnostic(
2663
2693
  diagnostics,
2664
2694
  ssml,
2665
2695
  token.start,
2666
2696
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2667
2697
  );
2698
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2668
2699
  }
2669
2700
  })
2670
2701
  );
2671
2702
  return Promise.all(checks).then(() => diagnostics);
2672
2703
  }
2704
+ async function validateAzureSsmlChunks(chunks, options = {}) {
2705
+ const validator = options.urlValidator ?? options.customUrlValidator;
2706
+ const sharedOptions = validator ? {
2707
+ ...options,
2708
+ urlValidatorRunner: options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2709
+ ...options.urlValidation ?? {},
2710
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2711
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2712
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2713
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2714
+ })
2715
+ } : options;
2716
+ return Promise.all(
2717
+ chunks.map((chunk, chunkIndex) => Promise.resolve(validateAzureSsml(chunk, { ...sharedOptions, chunkIndex })))
2718
+ );
2719
+ }
2673
2720
 
2674
2721
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2675
2722
  var AZURE_VOICE_CATALOG_METADATA = {
@@ -2730,6 +2777,14 @@ var MergeError = class extends Error {
2730
2777
  this.cause = cause;
2731
2778
  }
2732
2779
  };
2780
+ var AudioFormatMismatchError = class extends Error {
2781
+ constructor(message, inputSpecs = []) {
2782
+ super(message);
2783
+ this.kind = "audio-format-mismatch";
2784
+ this.name = "AudioFormatMismatchError";
2785
+ this.inputSpecs = inputSpecs;
2786
+ }
2787
+ };
2733
2788
  var UnsupportedMergeFormatError = class extends Error {
2734
2789
  constructor(format) {
2735
2790
  super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
@@ -2739,7 +2794,7 @@ var UnsupportedMergeFormatError = class extends Error {
2739
2794
  }
2740
2795
  };
2741
2796
  function toSynthesisError(error) {
2742
- if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
2797
+ if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
2743
2798
  return error;
2744
2799
  const message = error instanceof Error ? error.message : String(error);
2745
2800
  if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
@@ -3840,6 +3895,7 @@ function tokenizeElements2(source) {
3840
3895
  if (parent) parent.childElementCount += 1;
3841
3896
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
3842
3897
  const tokenName = nameMatch[1];
3898
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
3843
3899
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
3844
3900
  tokens.push({
3845
3901
  attributes,
@@ -3850,12 +3906,14 @@ function tokenizeElements2(source) {
3850
3906
  parentName: parent?.name,
3851
3907
  parentVoiceName,
3852
3908
  selfClosing,
3853
- start
3909
+ start,
3910
+ path
3854
3911
  });
3855
3912
  if (!selfClosing) {
3856
3913
  openElements.push({
3857
3914
  childElementCount: 0,
3858
3915
  name: tokenName,
3916
+ path,
3859
3917
  voiceName: tokenVoiceName
3860
3918
  });
3861
3919
  }
@@ -3868,13 +3926,14 @@ function location2(source, offset) {
3868
3926
  const line = before.split("\n").length;
3869
3927
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
3870
3928
  }
3871
- function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code) {
3929
+ function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
3872
3930
  diagnostics.push({
3873
3931
  ...location2(source, offset),
3874
3932
  message,
3875
3933
  severity,
3876
3934
  source: "ssml-static-validator",
3877
- ...code ? { code } : {}
3935
+ ...code ? { code } : {},
3936
+ ...metadata
3878
3937
  });
3879
3938
  }
3880
3939
  function isSupportedProsodyRate2(value) {
@@ -4337,9 +4396,11 @@ function validateAzureSsmlStatic2(ssml, options = {}) {
4337
4396
  for (const token of tokens) {
4338
4397
  const tokenName = token.name.toLowerCase();
4339
4398
  const tokenVoiceName = tokenName === "voice" ? attr2(token, "name")?.trim() : tokenName === "mstts:turn" ? attr2(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
4399
+ const tokenDiagnosticStart = diagnostics.length;
4340
4400
  validateElement2(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
4341
4401
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
4342
4402
  validateVoiceFeatureMatrix2(token, ssml, diagnostics, tokenVoiceName, definition);
4403
+ annotateTokenDiagnostics2(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
4343
4404
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
4344
4405
  addDiagnostic2(
4345
4406
  diagnostics,
@@ -4361,12 +4422,30 @@ function urlAttributes2(token) {
4361
4422
  return value === void 0 ? [] : [{ attribute, value }];
4362
4423
  });
4363
4424
  }
4425
+ function annotateTokenDiagnostics2(diagnostics, startIndex, token, options, voiceName) {
4426
+ const attributes = [...token.attributes.keys()];
4427
+ for (const diagnostic of diagnostics.slice(startIndex)) {
4428
+ const attributeName = attributes.find(
4429
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
4430
+ );
4431
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
4432
+ Object.assign(diagnostic, {
4433
+ range: { start: token.start, end: token.end + 1 },
4434
+ tagName: token.name,
4435
+ ...attributeName ? { attributeName } : {},
4436
+ ...voiceName ? { voiceName } : {},
4437
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
4438
+ nodePath,
4439
+ targetNodePath: [...token.path]
4440
+ });
4441
+ }
4442
+ }
4364
4443
  function validateAzureSsml2(ssml, options = {}) {
4365
4444
  const diagnostics = validateAzureSsmlStatic2(ssml, options);
4366
4445
  const validator = options.urlValidator ?? options.customUrlValidator;
4367
4446
  if (!validator || typeof ssml !== "string") return diagnostics;
4368
4447
  const runnerOptions = options.urlValidation ?? {};
4369
- const boundedValidator = createAzureUrlValidatorRunner2(validator, {
4448
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner2(validator, {
4370
4449
  ...runnerOptions,
4371
4450
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
4372
4451
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -4391,21 +4470,25 @@ function validateAzureSsml2(ssml, options = {}) {
4391
4470
  const valid = typeof result === "boolean" ? result : result.valid;
4392
4471
  if (!valid) {
4393
4472
  const reason = typeof result === "boolean" ? void 0 : result.reason;
4473
+ const diagnosticStart = diagnostics.length;
4394
4474
  addDiagnostic2(
4395
4475
  diagnostics,
4396
4476
  ssml,
4397
4477
  token.start,
4398
4478
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
4399
4479
  );
4480
+ annotateTokenDiagnostics2(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
4400
4481
  }
4401
4482
  } catch (error) {
4402
4483
  const reason = error instanceof Error ? error.message : String(error);
4484
+ const diagnosticStart = diagnostics.length;
4403
4485
  addDiagnostic2(
4404
4486
  diagnostics,
4405
4487
  ssml,
4406
4488
  token.start,
4407
4489
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
4408
4490
  );
4491
+ annotateTokenDiagnostics2(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
4409
4492
  }
4410
4493
  })
4411
4494
  );
@@ -4532,6 +4615,105 @@ function parseWav(buffer) {
4532
4615
  }
4533
4616
  return { chunks, data, format };
4534
4617
  }
4618
+ function formatNumber(format, pattern, fallback) {
4619
+ const match = pattern.exec(format);
4620
+ return match?.[1] ? Number(match[1]) : fallback;
4621
+ }
4622
+ function formatChannels(format, fallback) {
4623
+ if (/stereo|2ch|dual/i.test(format)) return 2;
4624
+ if (/mono|1ch/i.test(format)) return 1;
4625
+ return fallback;
4626
+ }
4627
+ function formatAudioSpecification(format) {
4628
+ const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
4629
+ const channels = formatChannels(format, 0);
4630
+ const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
4631
+ const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
4632
+ 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";
4633
+ return {
4634
+ format,
4635
+ mimeType: resolveMimeType(format),
4636
+ codec,
4637
+ sampleRate,
4638
+ channels,
4639
+ ...bitrate ? { bitrate } : {},
4640
+ isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
4641
+ };
4642
+ }
4643
+ function parseMp3Specification(buffer, format) {
4644
+ const bytes = stripMp3Tags(buffer);
4645
+ const bitrates = [
4646
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
4647
+ [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
4648
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
4649
+ ];
4650
+ const sampleRates = [
4651
+ [44100, 48e3, 32e3],
4652
+ [22050, 24e3, 16e3],
4653
+ [11025, 12e3, 8e3]
4654
+ ];
4655
+ for (let index = 0; index + 4 <= bytes.length; index += 1) {
4656
+ if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
4657
+ const header = bytes[index + 1] ?? 0;
4658
+ const versionBits = header >> 3 & 3;
4659
+ const layer = header >> 1 & 3;
4660
+ const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
4661
+ const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
4662
+ if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
4663
+ const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
4664
+ const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
4665
+ const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
4666
+ const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
4667
+ if (!sampleRate || !bitrateKbps) continue;
4668
+ return {
4669
+ format,
4670
+ mimeType: "audio/mpeg",
4671
+ codec: "mp3",
4672
+ sampleRate,
4673
+ channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
4674
+ bitrate: bitrateKbps * 1e3,
4675
+ isCompressed: true
4676
+ };
4677
+ }
4678
+ return void 0;
4679
+ }
4680
+ function inspectAudioSpecification(buffer, format) {
4681
+ if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
4682
+ const parsed = parseWav(buffer);
4683
+ if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
4684
+ const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
4685
+ const sampleRate = view.getUint32(4, true);
4686
+ const channels = view.getUint16(2, true);
4687
+ const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
4688
+ const formatCode = view.getUint16(0, true);
4689
+ return {
4690
+ format,
4691
+ mimeType: "audio/wav",
4692
+ codec: formatCode === 1 ? "pcm" : "unknown",
4693
+ sampleRate,
4694
+ channels,
4695
+ ...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
4696
+ isCompressed: formatCode !== 1
4697
+ };
4698
+ }
4699
+ if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
4700
+ return formatAudioSpecification(format);
4701
+ }
4702
+ function validateAudioSpecifications(specs) {
4703
+ const first = specs[0];
4704
+ if (!first) return;
4705
+ 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
4707
+ );
4708
+ if (mismatch)
4709
+ throw new AudioFormatMismatchError(
4710
+ `Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
4711
+ specs
4712
+ );
4713
+ }
4714
+ function isAudioFormatMismatch(error) {
4715
+ return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
4716
+ }
4535
4717
  function writeUint32(target, offset, value) {
4536
4718
  new DataView(target.buffer).setUint32(offset, value, true);
4537
4719
  }
@@ -4617,6 +4799,7 @@ function mergeAudioBuffers(buffers, options) {
4617
4799
  const format = typeof options === "string" ? options : options?.format;
4618
4800
  if (!format) throw new UnsupportedMergeFormatError("");
4619
4801
  try {
4802
+ validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
4620
4803
  if (isWavFormat(format)) return mergeWavBuffers(buffers);
4621
4804
  if (isMp3Format(format)) {
4622
4805
  const parts = buffers.map(stripMp3Tags);
@@ -4639,7 +4822,8 @@ function mergeAudioBuffers(buffers, options) {
4639
4822
  }
4640
4823
  throw new UnsupportedMergeFormatError(format);
4641
4824
  } catch (error) {
4642
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
4825
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
4826
+ throw error;
4643
4827
  throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
4644
4828
  }
4645
4829
  }
@@ -4717,17 +4901,26 @@ async function synthesizeSsml(ssml, config) {
4717
4901
  return {
4718
4902
  originalTextRange: { ...marker.originalTextRange },
4719
4903
  sourceNodePath: [...marker.sourceNodePath],
4720
- textRange: { ...marker.originalTextRange }
4904
+ textRange: { ...marker.originalTextRange },
4905
+ mappingStatus: "exact"
4721
4906
  };
4722
4907
  }
4723
- if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
4908
+ 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;
4912
+ }
4724
4913
  const value = text ?? "";
4725
4914
  let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
4726
- if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
4915
+ let mappingStatus = "exact";
4916
+ if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
4727
4917
  localStart = -1;
4918
+ mappingStatus = "fallback";
4919
+ }
4728
4920
  if (localStart < 0 || localStart > sourceText.length) {
4729
4921
  localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
4730
4922
  if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
4923
+ mappingStatus = "fallback";
4731
4924
  }
4732
4925
  localStart = Math.max(0, localStart);
4733
4926
  const localEnd = Math.min(sourceText.length, localStart + value.length);
@@ -4738,7 +4931,8 @@ async function synthesizeSsml(ssml, config) {
4738
4931
  return {
4739
4932
  originalTextRange: { ...fallbackRange },
4740
4933
  textRange: { ...fallbackRange },
4741
- ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
4934
+ ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
4935
+ mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
4742
4936
  };
4743
4937
  };
4744
4938
  synthesizer.wordBoundary = (_sender, event) => {
@@ -4787,20 +4981,27 @@ async function synthesizeSsml(ssml, config) {
4787
4981
  );
4788
4982
  const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
4789
4983
  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
- });
4984
+ const addSourceMetadata = (event) => {
4985
+ const mapped = {
4986
+ ...event,
4987
+ ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
4988
+ ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
4989
+ ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
4990
+ ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
4991
+ ...requestId ? { requestId } : {}
4992
+ };
4993
+ if (event.mappingStatus === "unmapped")
4994
+ Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
4995
+ return mapped;
4996
+ };
4798
4997
  const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
4799
4998
  const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
4800
4999
  const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
4801
5000
  resolve({
4802
5001
  audioData: result.audioData,
4803
5002
  durationMs,
5003
+ audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
5004
+ mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
4804
5005
  ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
4805
5006
  ...requestId ? { requestId } : {},
4806
5007
  ...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
@@ -4825,8 +5026,66 @@ async function synthesizeSsml(ssml, config) {
4825
5026
  }
4826
5027
  });
4827
5028
  }
5029
+ function isRetryableSynthesisError(error) {
5030
+ if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
5031
+ if (error instanceof AzureTtsError && error.status !== 0)
5032
+ return error.status === 429 || error.status >= 500 && error.status < 600;
5033
+ const message = error instanceof Error ? error.message : String(error);
5034
+ if (/\b4\d{2}\b/.test(message)) return false;
5035
+ const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
5036
+ if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
5037
+ return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
5038
+ }
5039
+ function retryDelay(options, retryAttempt) {
5040
+ const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
5041
+ return Math.floor(Math.random() * (base + 1));
5042
+ }
5043
+ function resolveConcurrency(value, total) {
5044
+ if (value === void 0) return 1;
5045
+ if (value === Infinity) return Math.max(1, total);
5046
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
5047
+ }
5048
+ async function waitForRetry(delayMs, signal) {
5049
+ if (signal?.aborted) throw new SynthesisCancelledError();
5050
+ if (delayMs <= 0) return;
5051
+ await new Promise((resolve, reject) => {
5052
+ let timer;
5053
+ const abort = () => {
5054
+ clearTimeout(timer);
5055
+ signal?.removeEventListener("abort", abort);
5056
+ reject(new SynthesisCancelledError());
5057
+ };
5058
+ timer = setTimeout(() => {
5059
+ signal?.removeEventListener("abort", abort);
5060
+ resolve();
5061
+ }, delayMs);
5062
+ if (signal) {
5063
+ signal.addEventListener("abort", abort, { once: true });
5064
+ }
5065
+ });
5066
+ }
5067
+ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
5068
+ const options = retryOptions ? {
5069
+ maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
5070
+ initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
5071
+ maxDelayMs: Math.max(0, retryOptions.maxDelayMs)
5072
+ } : void 0;
5073
+ let attempt = 0;
5074
+ while (true) {
5075
+ if (config.signal?.aborted) throw new SynthesisCancelledError();
5076
+ try {
5077
+ return await synthesizeSsml(ssml, config);
5078
+ } catch (error) {
5079
+ if (!options || attempt >= options.maxRetries || !isRetryableSynthesisError(error)) throw error;
5080
+ attempt += 1;
5081
+ const delayMs = retryDelay(options, attempt);
5082
+ onRetry(attempt, delayMs);
5083
+ await waitForRetry(delayMs, config.signal);
5084
+ }
5085
+ }
5086
+ }
4828
5087
  async function synthesizeSsmlChunks(chunks, config) {
4829
- const results = [];
5088
+ const results = new Array(chunks.length);
4830
5089
  const totalChunks = chunks.length;
4831
5090
  const report = (event) => config.onProgress?.(event);
4832
5091
  for (const [index, chunk] of chunks.entries()) {
@@ -4841,57 +5100,85 @@ async function synthesizeSsmlChunks(chunks, config) {
4841
5100
  durationMs: 0
4842
5101
  });
4843
5102
  }
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) {
5103
+ let completed = 0;
5104
+ let nextIndex = 0;
5105
+ const concurrency = resolveConcurrency(config.concurrency, chunks.length);
5106
+ const worker = async () => {
5107
+ while (true) {
5108
+ const index = nextIndex++;
5109
+ if (index >= chunks.length) return;
5110
+ const chunk = chunks[index];
5111
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
4877
5112
  report({
4878
- currentChunk: index,
5113
+ currentChunk: completed,
4879
5114
  totalChunks,
4880
- percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
5115
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
4881
5116
  chunkIndex: index,
4882
5117
  originalTextRange: input.originalTextRange,
4883
- status: "failed",
4884
- durationMs: Date.now() - startedAt,
4885
- error
5118
+ status: "synthesizing",
5119
+ durationMs: 0
4886
5120
  });
4887
- throw error;
5121
+ const startedAt = Date.now();
5122
+ try {
5123
+ const result = await synthesizeWithRetry(
5124
+ input.ssml,
5125
+ {
5126
+ ...config,
5127
+ ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
5128
+ ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
5129
+ ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
5130
+ ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
5131
+ chunkIndex: index,
5132
+ onProgress: void 0
5133
+ },
5134
+ config.retryOptions,
5135
+ (retryAttempt, nextRetryDelayMs) => report({
5136
+ currentChunk: completed,
5137
+ totalChunks,
5138
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5139
+ chunkIndex: index,
5140
+ originalTextRange: input.originalTextRange,
5141
+ status: "synthesizing",
5142
+ durationMs: Date.now() - startedAt,
5143
+ retryAttempt,
5144
+ nextRetryDelayMs,
5145
+ isRetrying: true
5146
+ })
5147
+ );
5148
+ results[index] = result;
5149
+ completed += 1;
5150
+ report({
5151
+ currentChunk: completed,
5152
+ totalChunks,
5153
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5154
+ chunkIndex: index,
5155
+ originalTextRange: input.originalTextRange,
5156
+ status: "success",
5157
+ durationMs: Date.now() - startedAt
5158
+ });
5159
+ } catch (error) {
5160
+ report({
5161
+ currentChunk: completed,
5162
+ totalChunks,
5163
+ percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
5164
+ chunkIndex: index,
5165
+ originalTextRange: input.originalTextRange,
5166
+ status: "failed",
5167
+ durationMs: Date.now() - startedAt,
5168
+ error
5169
+ });
5170
+ throw error;
5171
+ }
4888
5172
  }
4889
- }
4890
- return mergeSynthesisResults(results, {
4891
- format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
5173
+ };
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
4892
5179
  });
4893
5180
  }
4894
- function createMergedResult(results, audioData, format) {
5181
+ function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
4895
5182
  const boundaries = [];
4896
5183
  const visemes = [];
4897
5184
  const bookmarks = [];
@@ -4910,7 +5197,8 @@ function createMergedResult(results, audioData, format) {
4910
5197
  ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
4911
5198
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4912
5199
  ...textRange ? { textRange: { ...textRange } } : {},
4913
- ...requestId ? { requestId } : {}
5200
+ ...requestId ? { requestId } : {},
5201
+ mappingStatus: boundary.mappingStatus ?? "unmapped"
4914
5202
  });
4915
5203
  }
4916
5204
  for (const viseme of result.visemes ?? []) {
@@ -4925,7 +5213,8 @@ function createMergedResult(results, audioData, format) {
4925
5213
  ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
4926
5214
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4927
5215
  ...textRange ? { textRange: { ...textRange } } : {},
4928
- ...requestId ? { requestId } : {}
5216
+ ...requestId ? { requestId } : {},
5217
+ mappingStatus: viseme.mappingStatus ?? "unmapped"
4929
5218
  });
4930
5219
  }
4931
5220
  for (const bookmark of result.bookmarks ?? []) {
@@ -4940,7 +5229,8 @@ function createMergedResult(results, audioData, format) {
4940
5229
  ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
4941
5230
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
4942
5231
  ...textRange ? { textRange: { ...textRange } } : {},
4943
- ...requestId ? { requestId } : {}
5232
+ ...requestId ? { requestId } : {},
5233
+ mappingStatus: bookmark.mappingStatus ?? "unmapped"
4944
5234
  });
4945
5235
  }
4946
5236
  durationOffset += Math.max(0, result.durationMs);
@@ -4949,6 +5239,8 @@ function createMergedResult(results, audioData, format) {
4949
5239
  audioData,
4950
5240
  durationMs: durationOffset,
4951
5241
  mimeType: resolveMimeType(format),
5242
+ audioSpec: audioSpec ?? formatAudioSpecification(format),
5243
+ ...outputMimeType ? { mimeType: outputMimeType } : {},
4952
5244
  ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
4953
5245
  ...visemes.length > 0 ? { visemes } : {},
4954
5246
  ...bookmarks.length > 0 ? { bookmarks } : {},
@@ -4961,19 +5253,47 @@ function mergeSynthesisResults(results, options) {
4961
5253
  const format = resolvedOptions?.format;
4962
5254
  if (!format) throw new UnsupportedMergeFormatError("");
4963
5255
  const buffers = results.map((result) => result.audioData);
5256
+ const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
5257
+ validateAudioSpecifications(inputSpecs);
5258
+ const signal = resolvedOptions.signal ?? new AbortController().signal;
5259
+ if (signal.aborted) throw new SynthesisCancelledError();
4964
5260
  if (resolvedOptions.customMerger) {
4965
- return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
5261
+ return Promise.resolve().then(
5262
+ () => resolvedOptions.customMerger?.(buffers, {
5263
+ format,
5264
+ outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
5265
+ inputSpecs,
5266
+ signal
5267
+ })
5268
+ ).then((merged) => {
4966
5269
  if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
4967
- return createMergedResult(results, merged, format);
5270
+ if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
5271
+ throw new MergeError("The custom audio merger returned an invalid audio buffer.");
5272
+ if (signal.aborted) throw new SynthesisCancelledError();
5273
+ return createMergedResult(
5274
+ results,
5275
+ merged,
5276
+ format,
5277
+ inspectAudioSpecification(merged, format),
5278
+ resolvedOptions.outputMimeType
5279
+ );
4968
5280
  }).catch((error) => {
4969
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
5281
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5282
+ throw error;
4970
5283
  throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
4971
5284
  });
4972
5285
  }
4973
5286
  try {
4974
- return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
5287
+ return createMergedResult(
5288
+ results,
5289
+ mergeAudioBuffers(buffers, { format }),
5290
+ format,
5291
+ inputSpecs[0],
5292
+ resolvedOptions.outputMimeType
5293
+ );
4975
5294
  } catch (error) {
4976
- if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
5295
+ if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
5296
+ throw error;
4977
5297
  throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
4978
5298
  }
4979
5299
  }
@@ -4994,8 +5314,73 @@ var ChunkValidationError = class extends Error {
4994
5314
  function failure(error) {
4995
5315
  return { ok: false, success: false, status: error.kind, error };
4996
5316
  }
5317
+ function isRetryable(error) {
5318
+ if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
5319
+ const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
5320
+ if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
5321
+ const message = error instanceof Error ? error.message : String(error);
5322
+ if (/\b4\d{2}\b/.test(message)) return false;
5323
+ return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
5324
+ }
5325
+ function delayForRetry(options, attempt) {
5326
+ const maxDelay = Math.max(0, options.maxDelayMs);
5327
+ const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
5328
+ return Math.floor(Math.random() * (base + 1));
5329
+ }
5330
+ function resolveConcurrency2(value, total) {
5331
+ if (value === void 0) return 1;
5332
+ if (value === Infinity) return Math.max(1, total);
5333
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
5334
+ }
5335
+ async function retryableSynthesis(synthesize, options, signal, onRetry) {
5336
+ const retry = options ? {
5337
+ maxRetries: Math.max(0, Math.floor(options.maxRetries)),
5338
+ initialDelayMs: options.initialDelayMs,
5339
+ maxDelayMs: options.maxDelayMs
5340
+ } : void 0;
5341
+ let attempt = 0;
5342
+ while (true) {
5343
+ if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
5344
+ try {
5345
+ return await synthesize();
5346
+ } catch (error) {
5347
+ if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
5348
+ attempt += 1;
5349
+ const delayMs = delayForRetry(retry, attempt);
5350
+ onRetry(attempt, delayMs);
5351
+ if (delayMs > 0)
5352
+ await new Promise((resolve, reject) => {
5353
+ const timer = setTimeout(() => {
5354
+ signal?.removeEventListener("abort", abort);
5355
+ resolve();
5356
+ }, delayMs);
5357
+ const abort = () => {
5358
+ clearTimeout(timer);
5359
+ signal?.removeEventListener("abort", abort);
5360
+ reject(new Error("Speech synthesis was cancelled."));
5361
+ };
5362
+ signal?.addEventListener("abort", abort, { once: true });
5363
+ });
5364
+ }
5365
+ }
5366
+ }
5367
+ function sharedValidationOptions(options, signal) {
5368
+ const validator = options.urlValidator ?? options.customUrlValidator;
5369
+ if (!validator) return signal ? withValidationSignal(options, signal) : options;
5370
+ const runner = createAzureUrlValidatorRunner2(validator, {
5371
+ ...options.urlValidation ?? {},
5372
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
5373
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
5374
+ ...signal ? { signal } : {},
5375
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
5376
+ });
5377
+ return {
5378
+ ...withValidationSignal(options, signal),
5379
+ urlValidatorRunner: runner
5380
+ };
5381
+ }
4997
5382
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
4998
- const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
5383
+ const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
4999
5384
  const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
5000
5385
  if (options.signal?.aborted) {
5001
5386
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
@@ -5022,7 +5407,7 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
5022
5407
  }
5023
5408
  }
5024
5409
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5025
- const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
5410
+ const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
5026
5411
  if (options.signal?.aborted) {
5027
5412
  const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5028
5413
  return failure(error);
@@ -5043,16 +5428,24 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5043
5428
  pending(index, "pending");
5044
5429
  });
5045
5430
  const validations = await Promise.all(
5046
- chunks.map(async (chunk) => {
5431
+ chunks.map(async (chunk, index) => {
5047
5432
  const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
5048
5433
  const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
5049
5434
  const diagnostics = await Promise.resolve(
5050
- validateAzureSsml2(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
5435
+ validateAzureSsml2(ssml, {
5436
+ ...validationOptions,
5437
+ ...sourceNodePath ? { sourceNodePath } : {},
5438
+ chunkIndex: index
5439
+ })
5051
5440
  );
5052
5441
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
5053
5442
  })
5054
5443
  );
5055
5444
  const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
5445
+ if (options.signal?.aborted) {
5446
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
5447
+ return failure(error);
5448
+ }
5056
5449
  if (firstInvalidIndex >= 0) {
5057
5450
  const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
5058
5451
  pending(firstInvalidIndex, "failed", error);
@@ -5069,96 +5462,126 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
5069
5462
  outputFormat: options.outputFormat,
5070
5463
  signal: options.signal,
5071
5464
  timeoutMs: options.timeoutMs,
5072
- sourceNodePath: options.sourceNodePath
5465
+ sourceNodePath: options.sourceNodePath,
5466
+ concurrency: options.concurrency,
5467
+ retryOptions: options.retryOptions
5073
5468
  });
5074
5469
  return { ok: true, success: true, status: "success", value };
5075
5470
  }
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;
5471
+ const results = new Array(chunks.length);
5472
+ let completed = 0;
5473
+ let nextIndex = 0;
5474
+ const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
5475
+ const worker = async () => {
5476
+ while (true) {
5477
+ const index = nextIndex++;
5478
+ if (index >= chunks.length) return;
5479
+ const chunk = chunks[index];
5480
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
5481
+ const sourceNodePath = input.sourceNodePath;
5482
+ const originalTextRange = input.originalTextRange;
5483
+ pending(index, "synthesizing");
5484
+ const startedAt = Date.now();
5485
+ 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
+ );
5508
+ results[index] = {
5509
+ ...result,
5510
+ ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
5511
+ ...sourceNodePath ? {
5512
+ boundaries: result.boundaries?.map((event) => ({
5513
+ ...event,
5514
+ sourceNodePath: [...sourceNodePath],
5515
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5516
+ })),
5517
+ visemes: result.visemes?.map((event) => ({
5518
+ ...event,
5519
+ sourceNodePath: [...sourceNodePath],
5520
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5521
+ })),
5522
+ bookmarks: result.bookmarks?.map((event) => ({
5523
+ ...event,
5524
+ sourceNodePath: [...sourceNodePath],
5525
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
5526
+ }))
5527
+ } : {},
5528
+ ...originalTextRange ? {
5529
+ boundaries: result.boundaries?.map((event) => ({
5530
+ ...event,
5531
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5532
+ })),
5533
+ wordBoundary: result.wordBoundary?.map((event) => ({
5534
+ ...event,
5535
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5536
+ })),
5537
+ wordBoundaries: result.wordBoundaries?.map((event) => ({
5538
+ ...event,
5539
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5540
+ })),
5541
+ visemes: result.visemes?.map((event) => ({
5542
+ ...event,
5543
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5544
+ })),
5545
+ bookmarks: result.bookmarks?.map((event) => ({
5546
+ ...event,
5547
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
5548
+ }))
5549
+ } : {}
5550
+ };
5551
+ completed += 1;
5552
+ options.onProgress?.({
5553
+ currentChunk: completed,
5554
+ totalChunks: chunks.length,
5555
+ percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
5556
+ chunkIndex: index,
5557
+ originalTextRange: input.originalTextRange,
5558
+ status: "success",
5559
+ durationMs: Date.now() - startedAt
5560
+ });
5561
+ } catch (error) {
5562
+ options.onProgress?.({
5563
+ currentChunk: completed,
5564
+ totalChunks: chunks.length,
5565
+ percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
5566
+ chunkIndex: index,
5567
+ originalTextRange: input.originalTextRange,
5568
+ status: "failed",
5569
+ durationMs: Date.now() - startedAt,
5570
+ error
5571
+ });
5572
+ throw error;
5573
+ }
5154
5574
  }
5155
- }
5575
+ };
5576
+ await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
5577
+ const orderedResults = results.filter((result) => result !== void 0);
5156
5578
  return {
5157
5579
  ok: true,
5158
5580
  success: true,
5159
5581
  status: "success",
5160
- value: mergeSynthesisResults(results, {
5161
- format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
5582
+ value: mergeSynthesisResults(orderedResults, {
5583
+ format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
5584
+ signal: options.signal
5162
5585
  })
5163
5586
  };
5164
5587
  } catch (error) {
@@ -5217,7 +5640,9 @@ var AzureTtsClient = class {
5217
5640
  signal: options.signal ?? signal,
5218
5641
  timeoutMs: options.timeoutMs ?? timeoutMs,
5219
5642
  sourceNodePath: options.sourceNodePath,
5220
- onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
5643
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5644
+ concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
5645
+ retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
5221
5646
  });
5222
5647
  }
5223
5648
  async synthesizeSsmlSafe(ssml, options = {}) {
@@ -5229,7 +5654,9 @@ var AzureTtsClient = class {
5229
5654
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
5230
5655
  signal: options.signal ?? __privateGet(this, _options).signal,
5231
5656
  timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
5232
- onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
5657
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
5658
+ concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
5659
+ retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
5233
5660
  });
5234
5661
  }
5235
5662
  async synthesizeSsmlChunksSafe(chunks, options = {}) {
@@ -5326,6 +5753,7 @@ async function fetchAzureVoiceCatalog(options) {
5326
5753
  }
5327
5754
  // Annotate the CommonJS export names for ESM import in node:
5328
5755
  0 && (module.exports = {
5756
+ AudioFormatMismatchError,
5329
5757
  AzureTtsClient,
5330
5758
  AzureTtsError,
5331
5759
  AzureTtsSdkError,
@@ -5347,6 +5775,7 @@ async function fetchAzureVoiceCatalog(options) {
5347
5775
  getAzureVoiceCatalogMetadata,
5348
5776
  getBuiltInVoiceCatalogMetadata,
5349
5777
  getSsmlSourceMap,
5778
+ inspectAudioSpecification,
5350
5779
  isValidAzureAudioDuration,
5351
5780
  mapSsmlTextNodes,
5352
5781
  mergeAudioBuffers,
@@ -5362,6 +5791,7 @@ async function fetchAzureVoiceCatalog(options) {
5362
5791
  synthesizeSsmlChunksSafe,
5363
5792
  synthesizeSsmlSafe,
5364
5793
  validateAzureSsml,
5794
+ validateAzureSsmlChunks,
5365
5795
  validateSsml,
5366
5796
  validateSsmlStructureIntegrity
5367
5797
  });