ssml-builder-js 2.13.0 → 2.14.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
@@ -40,9 +40,13 @@ __export(src_exports, {
40
40
  AzureTtsClient: () => AzureTtsClient,
41
41
  AzureTtsError: () => AzureTtsError,
42
42
  AzureTtsSdkError: () => AzureTtsSdkError,
43
+ ChunkValidationError: () => ChunkValidationError,
44
+ UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
43
45
  areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
44
46
  buildPartialSsml: () => buildPartialSsml,
45
47
  buildSsml: () => buildSsml,
48
+ canMergeAudioFormat: () => canMergeAudioFormat,
49
+ createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
46
50
  extractSsmlText: () => extractSsmlText,
47
51
  extractSsmlTranslatableText: () => extractSsmlTranslatableText,
48
52
  fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
@@ -51,13 +55,16 @@ __export(src_exports, {
51
55
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
52
56
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
53
57
  mapSsmlTextNodes: () => mapSsmlTextNodes,
58
+ mergeAudioBuffers: () => mergeAudioBuffers,
54
59
  mergeSynthesisResults: () => mergeSynthesisResults,
55
60
  normalizeAzureLanguage: () => normalizeAzureLanguage,
56
61
  parseSsml: () => parseSsml,
62
+ resolveMergeAudioFormat: () => resolveMergeAudioFormat,
57
63
  splitSsmlDocument: () => splitSsmlDocument,
58
64
  synthesizeSpeech: () => synthesizeSpeech,
59
65
  synthesizeSsml: () => synthesizeSsml,
60
66
  synthesizeSsmlChunks: () => synthesizeSsmlChunks,
67
+ synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
61
68
  synthesizeSsmlSafe: () => synthesizeSsmlSafe,
62
69
  validateAzureSsml: () => validateAzureSsml,
63
70
  validateSsml: () => validateSsml,
@@ -1095,6 +1102,34 @@ function collectInheritedContext(nodes) {
1095
1102
  nodes.forEach(visit);
1096
1103
  return context;
1097
1104
  }
1105
+ function elementName(node) {
1106
+ return node.type === "custom" || node.type === "element" ? node.name : node.type;
1107
+ }
1108
+ function findSourceNodePath(nodes, targetOffset) {
1109
+ let textOffset = 0;
1110
+ let firstPath;
1111
+ let foundPath;
1112
+ const visit = (node, path) => {
1113
+ if (typeof node === "string" || node.type === "text") {
1114
+ const text = typeof node === "string" ? node : node.value;
1115
+ if (text && firstPath === void 0) firstPath = [...path];
1116
+ if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
1117
+ textOffset += text.length;
1118
+ return;
1119
+ }
1120
+ node.children?.forEach((child, index) => {
1121
+ const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
1122
+ visit(child, childPath);
1123
+ });
1124
+ };
1125
+ nodes.forEach((node, index) => {
1126
+ if (!foundPath) {
1127
+ if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
1128
+ else visit(node, ["speak", `${elementName(node)}[${index}]`]);
1129
+ }
1130
+ });
1131
+ return foundPath ?? firstPath;
1132
+ }
1098
1133
  function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1099
1134
  const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1100
1135
  const text = nodes.map(textFromNode).join("");
@@ -1110,7 +1145,8 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
1110
1145
  containedMarks: marks,
1111
1146
  hasBackgroundAudio: chunkNodes.some(
1112
1147
  (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1113
- )
1148
+ ),
1149
+ sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
1114
1150
  };
1115
1151
  }
1116
1152
  function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
@@ -1336,7 +1372,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1336
1372
 
1337
1373
  // packages/ssml-core/src/migration.ts
1338
1374
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1339
- function elementName(element) {
1375
+ function elementName2(element) {
1340
1376
  switch (element.type) {
1341
1377
  case "custom":
1342
1378
  case "element":
@@ -1489,7 +1525,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
1489
1525
  }
1490
1526
  return;
1491
1527
  }
1492
- const tag = elementName(node);
1528
+ const tag = elementName2(node);
1493
1529
  if (skipTags.has(tag.toLowerCase())) return;
1494
1530
  visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
1495
1531
  });
@@ -1535,7 +1571,7 @@ function serializeDocument2(document) {
1535
1571
  const serialize = (node) => {
1536
1572
  if (typeof node === "string") return node.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1537
1573
  if (node.type === "text") return serialize(node.value);
1538
- const tag = elementName(node);
1574
+ const tag = elementName2(node);
1539
1575
  const nodeAttributes = elementAttributes(node);
1540
1576
  const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, "&quot;")}"`).join("");
1541
1577
  const children = childrenOf(node).map(serialize).join("");
@@ -1549,7 +1585,7 @@ function flatten(document) {
1549
1585
  nodes.forEach((node, index) => {
1550
1586
  if (typeof node === "string" || node.type === "text") return;
1551
1587
  const currentPath = `${path}/${index}`;
1552
- result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
1588
+ result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
1553
1589
  visit(childrenOf(node), currentPath);
1554
1590
  });
1555
1591
  };
@@ -1747,6 +1783,69 @@ var AZURE_VOICE_DEFINITIONS = [
1747
1783
  ];
1748
1784
 
1749
1785
  // packages/ssml-core/src/azureValidation.ts
1786
+ function createAzureUrlValidatorRunner(validator, options = {}) {
1787
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
1788
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
1789
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
1790
+ const inFlight = /* @__PURE__ */ new Map();
1791
+ const waiters = [];
1792
+ let active = 0;
1793
+ const acquire = async () => {
1794
+ if (active < concurrency) {
1795
+ active += 1;
1796
+ return;
1797
+ }
1798
+ await new Promise((resolve) => waiters.push(resolve));
1799
+ active += 1;
1800
+ };
1801
+ const release = () => {
1802
+ active -= 1;
1803
+ waiters.shift()?.();
1804
+ };
1805
+ const check = async (url, context) => {
1806
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1807
+ const cached = cache.get(url);
1808
+ if (cached !== void 0) return cached;
1809
+ const existing = inFlight.get(url);
1810
+ if (existing) return existing;
1811
+ const promise = (async () => {
1812
+ await acquire();
1813
+ try {
1814
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
1815
+ const validation = Promise.resolve(validator(url, context));
1816
+ let timer;
1817
+ let abortHandler;
1818
+ const cancellation = new Promise((_resolve, reject) => {
1819
+ abortHandler = () => reject(new Error("URL validation was aborted."));
1820
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
1821
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
1822
+ timer = setTimeout(
1823
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
1824
+ options.timeoutMs
1825
+ );
1826
+ }
1827
+ });
1828
+ try {
1829
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
1830
+ cache.set(url, result);
1831
+ return result;
1832
+ } finally {
1833
+ if (timer) clearTimeout(timer);
1834
+ if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
1835
+ }
1836
+ } finally {
1837
+ release();
1838
+ }
1839
+ })();
1840
+ inFlight.set(url, promise);
1841
+ try {
1842
+ return await promise;
1843
+ } finally {
1844
+ inFlight.delete(url);
1845
+ }
1846
+ };
1847
+ return (url, context) => check(url, context);
1848
+ }
1750
1849
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1751
1850
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1752
1851
  "characters",
@@ -2041,23 +2140,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
2041
2140
  );
2042
2141
  }
2043
2142
  }
2044
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
2143
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
2045
2144
  const src = attr(token, "src");
2046
2145
  if (!src) {
2047
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2146
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
2048
2147
  return;
2049
2148
  }
2050
2149
  let parsed;
2051
2150
  try {
2052
2151
  parsed = new URL(src);
2053
2152
  } catch {
2054
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2153
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
2055
2154
  return;
2056
2155
  }
2057
2156
  if (parsed.username || parsed.password)
2058
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
2157
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
2059
2158
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2060
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2159
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
2061
2160
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
2062
2161
  try {
2063
2162
  const configured = new URL(allowedOrigin);
@@ -2069,13 +2168,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2069
2168
  }
2070
2169
  }) ?? false;
2071
2170
  if (options.allowedAudioOrigins && !isAllowedOrigin)
2072
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2171
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
2073
2172
  else if (!isAllowedOrigin && !options.allowExternalAudio)
2074
2173
  addDiagnostic(
2075
2174
  diagnostics,
2076
2175
  source,
2077
2176
  token.start,
2078
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2177
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2079
2178
  );
2080
2179
  }
2081
2180
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -2393,6 +2492,14 @@ function validateAzureSsml(ssml, options = {}) {
2393
2492
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2394
2493
  const validator = options.urlValidator ?? options.customUrlValidator;
2395
2494
  if (!validator || typeof ssml !== "string") return diagnostics;
2495
+ const runnerOptions = options.urlValidation ?? {};
2496
+ const boundedValidator = createAzureUrlValidatorRunner(validator, {
2497
+ ...runnerOptions,
2498
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2499
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2500
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2501
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2502
+ });
2396
2503
  let tokens;
2397
2504
  try {
2398
2505
  tokens = tokenizeElements(ssml);
@@ -2402,7 +2509,7 @@ function validateAzureSsml(ssml, options = {}) {
2402
2509
  const checks = tokens.flatMap(
2403
2510
  (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2404
2511
  try {
2405
- const result = await validator(value, { tag: token.name, attribute });
2512
+ const result = await boundedValidator(value, { tag: token.name, attribute });
2406
2513
  const valid = typeof result === "boolean" ? result : result.valid;
2407
2514
  if (!valid) {
2408
2515
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -2463,6 +2570,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
2463
2570
  this.errorDetails = errorDetails;
2464
2571
  }
2465
2572
  };
2573
+ var UnsupportedMergeFormatError = class extends Error {
2574
+ constructor(format) {
2575
+ super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
2576
+ this.name = "UnsupportedMergeFormatError";
2577
+ this.format = format;
2578
+ }
2579
+ };
2466
2580
  function createSpeechSdkError(error) {
2467
2581
  const message = error instanceof Error ? error.message : String(error);
2468
2582
  return new AzureTtsSdkError(message);
@@ -2540,6 +2654,149 @@ function createSpeechConfig(config) {
2540
2654
  }
2541
2655
 
2542
2656
  // packages/azure-tts-client/src/synthesis.ts
2657
+ function ascii(bytes, offset, value) {
2658
+ return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
2659
+ }
2660
+ function readUint32(bytes, offset) {
2661
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
2662
+ }
2663
+ function parseWav(buffer) {
2664
+ const bytes = new Uint8Array(buffer);
2665
+ if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
2666
+ throw new Error("Invalid WAV/RIFF audio buffer.");
2667
+ }
2668
+ const chunks = [];
2669
+ const dataParts = [];
2670
+ let format;
2671
+ let offset = 12;
2672
+ while (offset < bytes.byteLength) {
2673
+ if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
2674
+ const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
2675
+ const size = readUint32(bytes, offset + 4);
2676
+ const dataStart = offset + 8;
2677
+ const dataEnd = dataStart + size;
2678
+ if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
2679
+ const data2 = bytes.slice(dataStart, dataEnd);
2680
+ chunks.push({ id, data: data2 });
2681
+ if (id === "fmt ") format ?? (format = data2);
2682
+ if (id === "data") dataParts.push(data2);
2683
+ offset = dataEnd + (size & 1);
2684
+ if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
2685
+ }
2686
+ if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
2687
+ const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
2688
+ const data = new Uint8Array(dataLength);
2689
+ let dataOffset = 0;
2690
+ for (const part of dataParts) {
2691
+ data.set(part, dataOffset);
2692
+ dataOffset += part.byteLength;
2693
+ }
2694
+ return { chunks, data, format };
2695
+ }
2696
+ function writeUint32(target, offset, value) {
2697
+ new DataView(target.buffer).setUint32(offset, value, true);
2698
+ }
2699
+ function writeChunk(target, offset, id, data) {
2700
+ for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
2701
+ writeUint32(target, offset + 4, data.byteLength);
2702
+ target.set(data, offset + 8);
2703
+ const end = offset + 8 + data.byteLength;
2704
+ if (data.byteLength & 1) target[end] = 0;
2705
+ return end + (data.byteLength & 1);
2706
+ }
2707
+ function mergeWavBuffers(buffers) {
2708
+ if (buffers.length === 0) return new ArrayBuffer(0);
2709
+ const parsed = buffers.map(parseWav);
2710
+ const first = parsed[0];
2711
+ if (!first) throw new Error("At least one WAV buffer is required.");
2712
+ if (parsed.some(
2713
+ (item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
2714
+ ))
2715
+ throw new Error("WAV buffers have incompatible fmt chunks.");
2716
+ const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
2717
+ const nonDataLength = first.chunks.reduce(
2718
+ (total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
2719
+ 0
2720
+ );
2721
+ const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
2722
+ if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
2723
+ const output = new Uint8Array(outputLength);
2724
+ output.set(Uint8Array.from([82, 73, 70, 70]), 0);
2725
+ writeUint32(output, 4, outputLength - 8);
2726
+ output.set(Uint8Array.from([87, 65, 86, 69]), 8);
2727
+ let outputOffset = 12;
2728
+ let dataWritten = false;
2729
+ for (const chunk of first.chunks) {
2730
+ if (chunk.id === "data") {
2731
+ if (dataWritten) continue;
2732
+ const data = new Uint8Array(dataLength);
2733
+ let dataOffset = 0;
2734
+ for (const item of parsed) {
2735
+ data.set(item.data, dataOffset);
2736
+ dataOffset += item.data.byteLength;
2737
+ }
2738
+ outputOffset = writeChunk(output, outputOffset, "data", data);
2739
+ dataWritten = true;
2740
+ } else {
2741
+ outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
2742
+ }
2743
+ }
2744
+ if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
2745
+ return output.buffer;
2746
+ }
2747
+ function skipId3v2(bytes) {
2748
+ if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
2749
+ const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
2750
+ const hasFooter = (bytes[5] & 16) !== 0;
2751
+ return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
2752
+ }
2753
+ function stripMp3Tags(buffer) {
2754
+ const bytes = new Uint8Array(buffer);
2755
+ const start = skipId3v2(bytes);
2756
+ const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
2757
+ return bytes.slice(Math.min(start, end), end);
2758
+ }
2759
+ function isMp3Format(format) {
2760
+ return /(?:mp3|mpeg)/i.test(format);
2761
+ }
2762
+ function isWavFormat(format) {
2763
+ return /(?:wav|wave|riff)/i.test(format);
2764
+ }
2765
+ function isRawFormat(format) {
2766
+ return /^raw(?:-|$)/i.test(format);
2767
+ }
2768
+ function resolveMergeAudioFormat(format) {
2769
+ if (isWavFormat(format)) return "wav";
2770
+ if (isMp3Format(format)) return "mp3";
2771
+ if (isRawFormat(format)) return "raw";
2772
+ return void 0;
2773
+ }
2774
+ function canMergeAudioFormat(format) {
2775
+ return resolveMergeAudioFormat(format) !== void 0;
2776
+ }
2777
+ function mergeAudioBuffers(buffers, format) {
2778
+ if (isWavFormat(format)) return mergeWavBuffers(buffers);
2779
+ if (isMp3Format(format)) {
2780
+ const parts = buffers.map(stripMp3Tags);
2781
+ const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
2782
+ let offset = 0;
2783
+ for (const part of parts) {
2784
+ output.set(part, offset);
2785
+ offset += part.byteLength;
2786
+ }
2787
+ return output.buffer;
2788
+ }
2789
+ if (isRawFormat(format)) {
2790
+ const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
2791
+ let offset = 0;
2792
+ for (const buffer of buffers) {
2793
+ output.set(new Uint8Array(buffer), offset);
2794
+ offset += buffer.byteLength;
2795
+ }
2796
+ return output.buffer;
2797
+ }
2798
+ throw new UnsupportedMergeFormatError(format);
2799
+ }
2543
2800
  function closeSpeechResources(speechConfig, synthesizer) {
2544
2801
  try {
2545
2802
  synthesizer.close();
@@ -2616,6 +2873,9 @@ async function synthesizeSsml(ssml, config) {
2616
2873
  const addSourceMetadata = (event) => ({
2617
2874
  ...event,
2618
2875
  ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
2876
+ ...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
2877
+ ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
2878
+ ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
2619
2879
  ...requestId ? { requestId } : {}
2620
2880
  });
2621
2881
  const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
@@ -2651,60 +2911,126 @@ async function synthesizeSsml(ssml, config) {
2651
2911
  async function synthesizeSsmlChunks(chunks, config) {
2652
2912
  const results = [];
2653
2913
  const totalChunks = chunks.length;
2914
+ const report = (event) => config.onProgress?.(event);
2654
2915
  for (const [index, chunk] of chunks.entries()) {
2655
2916
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
2656
- const result = await synthesizeSsml(input.ssml, {
2657
- ...config,
2658
- ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
2659
- onProgress: void 0
2917
+ report({
2918
+ currentChunk: index,
2919
+ totalChunks,
2920
+ percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
2921
+ chunkIndex: index,
2922
+ originalTextRange: input.originalTextRange,
2923
+ status: "pending",
2924
+ durationMs: 0
2660
2925
  });
2661
- results.push(result);
2662
- config.onProgress?.({
2663
- currentChunk: index + 1,
2926
+ }
2927
+ for (const [index, chunk] of chunks.entries()) {
2928
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
2929
+ report({
2930
+ currentChunk: index,
2664
2931
  totalChunks,
2665
- percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100)
2932
+ percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
2933
+ chunkIndex: index,
2934
+ originalTextRange: input.originalTextRange,
2935
+ status: "synthesizing",
2936
+ durationMs: 0
2666
2937
  });
2938
+ const startedAt = Date.now();
2939
+ try {
2940
+ const result = await synthesizeSsml(input.ssml, {
2941
+ ...config,
2942
+ ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
2943
+ ...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
2944
+ chunkIndex: index,
2945
+ onProgress: void 0
2946
+ });
2947
+ results.push(result);
2948
+ report({
2949
+ currentChunk: index + 1,
2950
+ totalChunks,
2951
+ percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
2952
+ chunkIndex: index,
2953
+ originalTextRange: input.originalTextRange,
2954
+ status: "success",
2955
+ durationMs: Date.now() - startedAt
2956
+ });
2957
+ } catch (error) {
2958
+ report({
2959
+ currentChunk: index,
2960
+ totalChunks,
2961
+ percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
2962
+ chunkIndex: index,
2963
+ originalTextRange: input.originalTextRange,
2964
+ status: "failed",
2965
+ durationMs: Date.now() - startedAt,
2966
+ error
2967
+ });
2968
+ throw error;
2969
+ }
2667
2970
  }
2668
- return mergeSynthesisResults(results);
2971
+ return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
2669
2972
  }
2670
- function mergeSynthesisResults(results) {
2671
- const audioLength = results.reduce((total, result) => total + result.audioData.byteLength, 0);
2672
- const audioData = new Uint8Array(audioLength);
2973
+ function mergeSynthesisResults(results, format) {
2974
+ const audioData = format ? new Uint8Array(
2975
+ mergeAudioBuffers(
2976
+ results.map((result) => result.audioData),
2977
+ format
2978
+ )
2979
+ ) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
2980
+ if (!format) {
2981
+ let offset = 0;
2982
+ for (const result of results) {
2983
+ audioData.set(new Uint8Array(result.audioData), offset);
2984
+ offset += result.audioData.byteLength;
2985
+ }
2986
+ }
2673
2987
  const boundaries = [];
2674
2988
  const visemes = [];
2675
2989
  const bookmarks = [];
2676
- let byteOffset = 0;
2677
2990
  let durationOffset = 0;
2678
2991
  for (const result of results) {
2679
- audioData.set(new Uint8Array(result.audioData), byteOffset);
2680
- byteOffset += result.audioData.byteLength;
2681
2992
  const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
2682
2993
  for (const boundary of chunkBoundaries) {
2683
2994
  const textRange = boundary.textRange ?? result.textRange;
2995
+ const originalTextRange = boundary.originalTextRange ?? textRange;
2684
2996
  const requestId = boundary.requestId ?? result.requestId;
2685
2997
  boundaries.push({
2686
2998
  ...boundary,
2687
2999
  audioOffsetMs: boundary.audioOffsetMs + durationOffset,
3000
+ chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
3001
+ ...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
3002
+ ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
3003
+ ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
2688
3004
  ...textRange ? { textRange: { ...textRange } } : {},
2689
3005
  ...requestId ? { requestId } : {}
2690
3006
  });
2691
3007
  }
2692
3008
  for (const viseme of result.visemes ?? []) {
2693
3009
  const textRange = viseme.textRange ?? result.textRange;
3010
+ const originalTextRange = viseme.originalTextRange ?? textRange;
2694
3011
  const requestId = viseme.requestId ?? result.requestId;
2695
3012
  visemes.push({
2696
3013
  ...viseme,
2697
3014
  audioOffsetMs: viseme.audioOffsetMs + durationOffset,
3015
+ chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
3016
+ ...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
3017
+ ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
3018
+ ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
2698
3019
  ...textRange ? { textRange: { ...textRange } } : {},
2699
3020
  ...requestId ? { requestId } : {}
2700
3021
  });
2701
3022
  }
2702
3023
  for (const bookmark of result.bookmarks ?? []) {
2703
3024
  const textRange = bookmark.textRange ?? result.textRange;
3025
+ const originalTextRange = bookmark.originalTextRange ?? textRange;
2704
3026
  const requestId = bookmark.requestId ?? result.requestId;
2705
3027
  bookmarks.push({
2706
3028
  ...bookmark,
2707
3029
  audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
3030
+ chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
3031
+ ...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
3032
+ ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
3033
+ ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
2708
3034
  ...textRange ? { textRange: { ...textRange } } : {},
2709
3035
  ...requestId ? { requestId } : {}
2710
3036
  });
@@ -3496,6 +3822,69 @@ var AZURE_VOICE_DEFINITIONS2 = [
3496
3822
  },
3497
3823
  { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
3498
3824
  ];
3825
+ function createAzureUrlValidatorRunner2(validator, options = {}) {
3826
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
3827
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
3828
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
3829
+ const inFlight = /* @__PURE__ */ new Map();
3830
+ const waiters = [];
3831
+ let active = 0;
3832
+ const acquire = async () => {
3833
+ if (active < concurrency) {
3834
+ active += 1;
3835
+ return;
3836
+ }
3837
+ await new Promise((resolve) => waiters.push(resolve));
3838
+ active += 1;
3839
+ };
3840
+ const release = () => {
3841
+ active -= 1;
3842
+ waiters.shift()?.();
3843
+ };
3844
+ const check = async (url, context) => {
3845
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
3846
+ const cached = cache.get(url);
3847
+ if (cached !== void 0) return cached;
3848
+ const existing = inFlight.get(url);
3849
+ if (existing) return existing;
3850
+ const promise = (async () => {
3851
+ await acquire();
3852
+ try {
3853
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
3854
+ const validation = Promise.resolve(validator(url, context));
3855
+ let timer;
3856
+ let abortHandler;
3857
+ const cancellation = new Promise((_resolve, reject) => {
3858
+ abortHandler = () => reject(new Error("URL validation was aborted."));
3859
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
3860
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
3861
+ timer = setTimeout(
3862
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
3863
+ options.timeoutMs
3864
+ );
3865
+ }
3866
+ });
3867
+ try {
3868
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
3869
+ cache.set(url, result);
3870
+ return result;
3871
+ } finally {
3872
+ if (timer) clearTimeout(timer);
3873
+ if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
3874
+ }
3875
+ } finally {
3876
+ release();
3877
+ }
3878
+ })();
3879
+ inFlight.set(url, promise);
3880
+ try {
3881
+ return await promise;
3882
+ } finally {
3883
+ inFlight.delete(url);
3884
+ }
3885
+ };
3886
+ return (url, context) => check(url, context);
3887
+ }
3499
3888
  var ALLOWED_BREAK_STRENGTHS2 = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
3500
3889
  var ALLOWED_SAY_AS2 = /* @__PURE__ */ new Set([
3501
3890
  "characters",
@@ -3780,23 +4169,23 @@ function validateVoiceFeatureMatrix2(token, source, diagnostics, voiceName, defi
3780
4169
  );
3781
4170
  }
3782
4171
  }
3783
- function validateAudioSource2(token, source, diagnostics, options, elementName2) {
4172
+ function validateAudioSource2(token, source, diagnostics, options, elementName3) {
3784
4173
  const src = attr2(token, "src");
3785
4174
  if (!src) {
3786
- addDiagnostic2(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
4175
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
3787
4176
  return;
3788
4177
  }
3789
4178
  let parsed;
3790
4179
  try {
3791
4180
  parsed = new URL(src);
3792
4181
  } catch {
3793
- addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
4182
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
3794
4183
  return;
3795
4184
  }
3796
4185
  if (parsed.username || parsed.password)
3797
- addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
4186
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
3798
4187
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
3799
- addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
4188
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
3800
4189
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3801
4190
  try {
3802
4191
  const configured = new URL(allowedOrigin);
@@ -3808,13 +4197,13 @@ function validateAudioSource2(token, source, diagnostics, options, elementName2)
3808
4197
  }
3809
4198
  }) ?? false;
3810
4199
  if (options.allowedAudioOrigins && !isAllowedOrigin)
3811
- addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
4200
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
3812
4201
  else if (!isAllowedOrigin && !options.allowExternalAudio)
3813
4202
  addDiagnostic2(
3814
4203
  diagnostics,
3815
4204
  source,
3816
4205
  token.start,
3817
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
4206
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
3818
4207
  );
3819
4208
  }
3820
4209
  function validateElement2(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -4132,6 +4521,14 @@ function validateAzureSsml2(ssml, options = {}) {
4132
4521
  const diagnostics = validateAzureSsmlStatic2(ssml, options);
4133
4522
  const validator = options.urlValidator ?? options.customUrlValidator;
4134
4523
  if (!validator || typeof ssml !== "string") return diagnostics;
4524
+ const runnerOptions = options.urlValidation ?? {};
4525
+ const boundedValidator = createAzureUrlValidatorRunner2(validator, {
4526
+ ...runnerOptions,
4527
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
4528
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
4529
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
4530
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
4531
+ });
4135
4532
  let tokens;
4136
4533
  try {
4137
4534
  tokens = tokenizeElements2(ssml);
@@ -4141,7 +4538,7 @@ function validateAzureSsml2(ssml, options = {}) {
4141
4538
  const checks = tokens.flatMap(
4142
4539
  (token) => urlAttributes2(token).map(async ({ attribute, value }) => {
4143
4540
  try {
4144
- const result = await validator(value, { tag: token.name, attribute });
4541
+ const result = await boundedValidator(value, { tag: token.name, attribute });
4145
4542
  const valid = typeof result === "boolean" ? result : result.valid;
4146
4543
  if (!valid) {
4147
4544
  const reason = typeof result === "boolean" ? void 0 : result.reason;
@@ -4173,6 +4570,15 @@ var AZURE_VOICE_CATALOG_METADATA2 = {
4173
4570
  };
4174
4571
 
4175
4572
  // packages/azure-tts-client/src/safe.ts
4573
+ var ChunkValidationError = class extends Error {
4574
+ constructor(chunkIndex, diagnostics) {
4575
+ super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
4576
+ this.kind = "chunk-validation";
4577
+ this.name = "ChunkValidationError";
4578
+ this.chunkIndex = chunkIndex;
4579
+ this.diagnostics = diagnostics;
4580
+ }
4581
+ };
4176
4582
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
4177
4583
  const validationOptions = options.validation ?? options;
4178
4584
  const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
@@ -4196,6 +4602,95 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
4196
4602
  return { ok: false, success: false, status: "azure-api-error", error: azureError };
4197
4603
  }
4198
4604
  }
4605
+ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
4606
+ const validationOptions = options.validation ?? options;
4607
+ const pending = (index, status, error) => {
4608
+ options.onProgress?.({
4609
+ currentChunk: status === "success" ? index + 1 : index,
4610
+ totalChunks: chunks.length,
4611
+ percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
4612
+ chunkIndex: index,
4613
+ originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
4614
+ status,
4615
+ durationMs: 0,
4616
+ ...error ? { error } : {}
4617
+ });
4618
+ };
4619
+ chunks.forEach((_chunk, index) => {
4620
+ pending(index, "pending");
4621
+ });
4622
+ const validations = await Promise.all(
4623
+ chunks.map(async (chunk) => {
4624
+ const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
4625
+ const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
4626
+ return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
4627
+ })
4628
+ );
4629
+ const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
4630
+ if (firstInvalidIndex >= 0) {
4631
+ const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
4632
+ pending(firstInvalidIndex, "failed", error);
4633
+ return { ok: false, success: false, status: "validation-error", error };
4634
+ }
4635
+ try {
4636
+ if (client.synthesizeChunks) {
4637
+ const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
4638
+ return { ok: true, success: true, status: "success", value };
4639
+ }
4640
+ const results = [];
4641
+ for (const [index, chunk] of chunks.entries()) {
4642
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
4643
+ const sourceNodePath = input.sourceNodePath;
4644
+ pending(index, "synthesizing");
4645
+ const startedAt = Date.now();
4646
+ try {
4647
+ const result = await client.synthesizeSsml(input.ssml);
4648
+ results.push({
4649
+ ...result,
4650
+ ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
4651
+ ...sourceNodePath ? {
4652
+ boundaries: result.boundaries?.map((event) => ({
4653
+ ...event,
4654
+ sourceNodePath: [...sourceNodePath]
4655
+ })),
4656
+ visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
4657
+ bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
4658
+ } : {}
4659
+ });
4660
+ options.onProgress?.({
4661
+ currentChunk: index + 1,
4662
+ totalChunks: chunks.length,
4663
+ percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
4664
+ chunkIndex: index,
4665
+ originalTextRange: input.originalTextRange,
4666
+ status: "success",
4667
+ durationMs: Date.now() - startedAt
4668
+ });
4669
+ } catch (error) {
4670
+ options.onProgress?.({
4671
+ currentChunk: index,
4672
+ totalChunks: chunks.length,
4673
+ percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
4674
+ chunkIndex: index,
4675
+ originalTextRange: input.originalTextRange,
4676
+ status: "failed",
4677
+ durationMs: Date.now() - startedAt,
4678
+ error
4679
+ });
4680
+ throw error;
4681
+ }
4682
+ }
4683
+ return {
4684
+ ok: true,
4685
+ success: true,
4686
+ status: "success",
4687
+ value: mergeSynthesisResults(results, options.outputFormat)
4688
+ };
4689
+ } catch (error) {
4690
+ const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
4691
+ return { ok: false, success: false, status: "azure-api-error", error: azureError };
4692
+ }
4693
+ }
4199
4694
 
4200
4695
  // packages/azure-tts-client/src/client.ts
4201
4696
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -4234,6 +4729,16 @@ var AzureTtsClient = class {
4234
4729
  async synthesizeSsmlSafe(ssml, options = {}) {
4235
4730
  return synthesizeSsmlSafe(this, ssml, options);
4236
4731
  }
4732
+ async synthesizeChunksSafe(chunks, options = {}) {
4733
+ return synthesizeSsmlChunksSafe(this, chunks, {
4734
+ ...options,
4735
+ outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
4736
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
4737
+ });
4738
+ }
4739
+ async synthesizeSsmlChunksSafe(chunks, options = {}) {
4740
+ return this.synthesizeChunksSafe(chunks, options);
4741
+ }
4237
4742
  };
4238
4743
  _options = new WeakMap();
4239
4744
 
@@ -4289,6 +4794,9 @@ async function fetchAzureVoiceCatalog(options) {
4289
4794
  const secondaryLocales = stringList(record.SecondaryLocaleList);
4290
4795
  const styles = stringList(record.StyleList);
4291
4796
  const status = normalizeStatus(record.Status);
4797
+ const supportedTags = stringList(record.SupportedTags);
4798
+ const unsupportedTags = stringList(record.UnsupportedTags);
4799
+ const models = stringList(record.Models);
4292
4800
  const merged = {
4293
4801
  name: existing?.name ?? name,
4294
4802
  locale: existing?.locale ?? locale,
@@ -4298,6 +4806,12 @@ async function fetchAzureVoiceCatalog(options) {
4298
4806
  if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
4299
4807
  const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
4300
4808
  if (mergedStyles.length > 0) merged.styles = mergedStyles;
4809
+ const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
4810
+ if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
4811
+ const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
4812
+ if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
4813
+ const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
4814
+ if (mergedModels.length > 0) merged.models = mergedModels;
4301
4815
  if (status) merged.status = status;
4302
4816
  else if (existing?.status) merged.status = existing.status;
4303
4817
  voices.set(key, merged);
@@ -4319,9 +4833,13 @@ async function fetchAzureVoiceCatalog(options) {
4319
4833
  AzureTtsClient,
4320
4834
  AzureTtsError,
4321
4835
  AzureTtsSdkError,
4836
+ ChunkValidationError,
4837
+ UnsupportedMergeFormatError,
4322
4838
  areAzureLanguagesEquivalent,
4323
4839
  buildPartialSsml,
4324
4840
  buildSsml,
4841
+ canMergeAudioFormat,
4842
+ createAzureUrlValidatorRunner,
4325
4843
  extractSsmlText,
4326
4844
  extractSsmlTranslatableText,
4327
4845
  fetchAzureVoiceCatalog,
@@ -4330,13 +4848,16 @@ async function fetchAzureVoiceCatalog(options) {
4330
4848
  getBuiltInVoiceCatalogMetadata,
4331
4849
  isValidAzureAudioDuration,
4332
4850
  mapSsmlTextNodes,
4851
+ mergeAudioBuffers,
4333
4852
  mergeSynthesisResults,
4334
4853
  normalizeAzureLanguage,
4335
4854
  parseSsml,
4855
+ resolveMergeAudioFormat,
4336
4856
  splitSsmlDocument,
4337
4857
  synthesizeSpeech,
4338
4858
  synthesizeSsml,
4339
4859
  synthesizeSsmlChunks,
4860
+ synthesizeSsmlChunksSafe,
4340
4861
  synthesizeSsmlSafe,
4341
4862
  validateAzureSsml,
4342
4863
  validateSsml,