ssml-builder-js 2.12.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,11 +55,17 @@ __export(src_exports, {
51
55
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
52
56
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
53
57
  mapSsmlTextNodes: () => mapSsmlTextNodes,
58
+ mergeAudioBuffers: () => mergeAudioBuffers,
59
+ mergeSynthesisResults: () => mergeSynthesisResults,
54
60
  normalizeAzureLanguage: () => normalizeAzureLanguage,
55
61
  parseSsml: () => parseSsml,
62
+ resolveMergeAudioFormat: () => resolveMergeAudioFormat,
56
63
  splitSsmlDocument: () => splitSsmlDocument,
57
64
  synthesizeSpeech: () => synthesizeSpeech,
58
65
  synthesizeSsml: () => synthesizeSsml,
66
+ synthesizeSsmlChunks: () => synthesizeSsmlChunks,
67
+ synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
68
+ synthesizeSsmlSafe: () => synthesizeSsmlSafe,
59
69
  validateAzureSsml: () => validateAzureSsml,
60
70
  validateSsml: () => validateSsml,
61
71
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
@@ -1061,30 +1071,138 @@ function splitNode(document, node, maxLength, context = []) {
1061
1071
  flush();
1062
1072
  return parts;
1063
1073
  }
1064
- function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH) {
1065
- if (!Number.isInteger(maxLength) || maxLength <= 0) {
1074
+ function textFromNode(node) {
1075
+ if (typeof node === "string") return node;
1076
+ if (node.type === "text") return node.value;
1077
+ return (node.children ?? []).map(textFromNode).join("");
1078
+ }
1079
+ function collectMarks(node, marks) {
1080
+ if (typeof node === "string" || node.type === "text") return;
1081
+ if (node.type === "mark" && node.name) marks.push(node.name);
1082
+ if (node.type === "bookmark" && node.mark) marks.push(node.mark);
1083
+ for (const child of node.children ?? []) collectMarks(child, marks);
1084
+ }
1085
+ function collectInheritedContext(nodes) {
1086
+ const context = {};
1087
+ const visit = (node) => {
1088
+ if (typeof node === "string" || node.type === "text") return;
1089
+ if (context.voice === void 0 && node.type === "voice" && node.name) context.voice = node.name;
1090
+ if (context.lang === void 0 && node.type === "lang" && node.lang) context.lang = node.lang;
1091
+ if (context.prosody === void 0 && node.type === "prosody") {
1092
+ const prosody = {};
1093
+ for (const [key, value] of Object.entries(node.attributes ?? {})) prosody[key] = String(value);
1094
+ for (const key of ["rate", "pitch", "volume", "contour", "range"]) {
1095
+ const value = node[key];
1096
+ if (value !== void 0) prosody[key] = String(value);
1097
+ }
1098
+ if (Object.keys(prosody).length > 0) context.prosody = prosody;
1099
+ }
1100
+ for (const child of node.children ?? []) visit(child);
1101
+ };
1102
+ nodes.forEach(visit);
1103
+ return context;
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
+ }
1133
+ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1134
+ const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1135
+ const text = nodes.map(textFromNode).join("");
1136
+ const marks = [];
1137
+ for (const node of nodes) collectMarks(node, marks);
1138
+ const inheritedContext = collectInheritedContext(nodes);
1139
+ if (inheritedContext.lang === void 0 && document.lang) inheritedContext.lang = document.lang;
1140
+ return {
1141
+ chunkIndex,
1142
+ ssml: documentWithChildren(document, chunkNodes),
1143
+ originalTextRange: { start: textStart, end: textStart + text.length },
1144
+ inheritedContext,
1145
+ containedMarks: marks,
1146
+ hasBackgroundAudio: chunkNodes.some(
1147
+ (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1148
+ ),
1149
+ sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
1150
+ };
1151
+ }
1152
+ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1153
+ const resolvedMaxLength = typeof maxLength === "number" ? maxLength : maxLength.maxLength ?? DEFAULT_MAX_LENGTH;
1154
+ const resolvedOptions = typeof maxLength === "number" ? options : maxLength;
1155
+ if (!Number.isInteger(resolvedMaxLength) || resolvedMaxLength <= 0) {
1066
1156
  throw new RangeError("maxLength must be a positive integer");
1067
1157
  }
1068
1158
  const document = parseSsml(ssml);
1069
- if (ssml.length <= maxLength) return [ssml];
1070
- const children = document.children ?? [];
1071
- const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength));
1159
+ const backgroundAudio = (document.children ?? []).find(
1160
+ (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1161
+ );
1162
+ if (ssml.length <= resolvedMaxLength) {
1163
+ return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
1164
+ }
1165
+ const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
1166
+ const plainDocumentLength = documentWithChildren(document, []).length;
1167
+ const backgroundDocumentLength = backgroundAudio ? documentWithChildren(document, [backgroundAudio]).length : plainDocumentLength;
1168
+ const backgroundOverhead = Math.max(0, backgroundDocumentLength - plainDocumentLength);
1169
+ const contentMaxLength = Math.max(1, resolvedMaxLength - backgroundOverhead);
1170
+ const splitChildren = contentChildren.flatMap((child) => splitNode(document, child, contentMaxLength));
1072
1171
  const chunks = [];
1073
1172
  let group = [];
1074
1173
  for (const child of splitChildren) {
1075
1174
  const candidate = [...group, child];
1076
- if (documentWithChildren(document, candidate).length <= maxLength) {
1175
+ if (documentWithChildren(document, candidate).length <= contentMaxLength) {
1077
1176
  group = candidate;
1078
1177
  continue;
1079
1178
  }
1080
1179
  if (group.length > 0) chunks.push(group);
1081
1180
  group = [child];
1082
- if (documentWithChildren(document, group).length > maxLength) {
1181
+ if (documentWithChildren(document, group).length > contentMaxLength) {
1083
1182
  throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1084
1183
  }
1085
1184
  }
1086
1185
  if (group.length > 0) chunks.push(group);
1087
- return chunks.map((chunk) => documentWithChildren(document, chunk));
1186
+ if (chunks.length === 0) {
1187
+ const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
1188
+ if (result.ssml.length > resolvedMaxLength) {
1189
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1190
+ }
1191
+ return [result];
1192
+ }
1193
+ let textStart = 0;
1194
+ return chunks.map((chunk, chunkIndex) => {
1195
+ const result = createChunk(
1196
+ document,
1197
+ chunk,
1198
+ chunkIndex,
1199
+ textStart,
1200
+ backgroundAudio,
1201
+ resolvedOptions.replicateBackgroundAudio ?? false
1202
+ );
1203
+ textStart = result.originalTextRange.end;
1204
+ return result;
1205
+ });
1088
1206
  }
1089
1207
 
1090
1208
  // packages/ssml-core/src/validation.ts
@@ -1254,7 +1372,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1254
1372
 
1255
1373
  // packages/ssml-core/src/migration.ts
1256
1374
  var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1257
- function elementName(element) {
1375
+ function elementName2(element) {
1258
1376
  switch (element.type) {
1259
1377
  case "custom":
1260
1378
  case "element":
@@ -1407,7 +1525,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
1407
1525
  }
1408
1526
  return;
1409
1527
  }
1410
- const tag = elementName(node);
1528
+ const tag = elementName2(node);
1411
1529
  if (skipTags.has(tag.toLowerCase())) return;
1412
1530
  visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
1413
1531
  });
@@ -1453,7 +1571,7 @@ function serializeDocument2(document) {
1453
1571
  const serialize = (node) => {
1454
1572
  if (typeof node === "string") return node.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1455
1573
  if (node.type === "text") return serialize(node.value);
1456
- const tag = elementName(node);
1574
+ const tag = elementName2(node);
1457
1575
  const nodeAttributes = elementAttributes(node);
1458
1576
  const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, "&quot;")}"`).join("");
1459
1577
  const children = childrenOf(node).map(serialize).join("");
@@ -1467,7 +1585,7 @@ function flatten(document) {
1467
1585
  nodes.forEach((node, index) => {
1468
1586
  if (typeof node === "string" || node.type === "text") return;
1469
1587
  const currentPath = `${path}/${index}`;
1470
- result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
1588
+ result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
1471
1589
  visit(childrenOf(node), currentPath);
1472
1590
  });
1473
1591
  };
@@ -1665,6 +1783,69 @@ var AZURE_VOICE_DEFINITIONS = [
1665
1783
  ];
1666
1784
 
1667
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
+ }
1668
1849
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1669
1850
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1670
1851
  "characters",
@@ -1959,23 +2140,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
1959
2140
  );
1960
2141
  }
1961
2142
  }
1962
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
2143
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
1963
2144
  const src = attr(token, "src");
1964
2145
  if (!src) {
1965
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2146
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
1966
2147
  return;
1967
2148
  }
1968
2149
  let parsed;
1969
2150
  try {
1970
2151
  parsed = new URL(src);
1971
2152
  } catch {
1972
- 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.`);
1973
2154
  return;
1974
2155
  }
1975
2156
  if (parsed.username || parsed.password)
1976
- 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.`);
1977
2158
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1978
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2159
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
1979
2160
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1980
2161
  try {
1981
2162
  const configured = new URL(allowedOrigin);
@@ -1987,13 +2168,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1987
2168
  }
1988
2169
  }) ?? false;
1989
2170
  if (options.allowedAudioOrigins && !isAllowedOrigin)
1990
- 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.`);
1991
2172
  else if (!isAllowedOrigin && !options.allowExternalAudio)
1992
2173
  addDiagnostic(
1993
2174
  diagnostics,
1994
2175
  source,
1995
2176
  token.start,
1996
- `<${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.`
1997
2178
  );
1998
2179
  }
1999
2180
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -2175,7 +2356,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
2175
2356
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
2176
2357
  }
2177
2358
  }
2178
- function validateAzureSsml(ssml, options = {}) {
2359
+ function validateAzureSsmlStatic(ssml, options = {}) {
2179
2360
  const diagnostics = [];
2180
2361
  if (typeof ssml !== "string") {
2181
2362
  return [
@@ -2299,6 +2480,59 @@ function validateAzureSsml(ssml, options = {}) {
2299
2480
  }
2300
2481
  return diagnostics;
2301
2482
  }
2483
+ function urlAttributes(token) {
2484
+ const tag = canonicalTagName(token.name);
2485
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
2486
+ return attributes.flatMap((attribute) => {
2487
+ const value = attr(token, attribute);
2488
+ return value === void 0 ? [] : [{ attribute, value }];
2489
+ });
2490
+ }
2491
+ function validateAzureSsml(ssml, options = {}) {
2492
+ const diagnostics = validateAzureSsmlStatic(ssml, options);
2493
+ const validator = options.urlValidator ?? options.customUrlValidator;
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
+ });
2503
+ let tokens;
2504
+ try {
2505
+ tokens = tokenizeElements(ssml);
2506
+ } catch {
2507
+ return diagnostics;
2508
+ }
2509
+ const checks = tokens.flatMap(
2510
+ (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2511
+ try {
2512
+ const result = await boundedValidator(value, { tag: token.name, attribute });
2513
+ const valid = typeof result === "boolean" ? result : result.valid;
2514
+ if (!valid) {
2515
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
2516
+ addDiagnostic(
2517
+ diagnostics,
2518
+ ssml,
2519
+ token.start,
2520
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2521
+ );
2522
+ }
2523
+ } catch (error) {
2524
+ const reason = error instanceof Error ? error.message : String(error);
2525
+ addDiagnostic(
2526
+ diagnostics,
2527
+ ssml,
2528
+ token.start,
2529
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2530
+ );
2531
+ }
2532
+ })
2533
+ );
2534
+ return Promise.all(checks).then(() => diagnostics);
2535
+ }
2302
2536
 
2303
2537
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2304
2538
  var AZURE_VOICE_CATALOG_METADATA = {
@@ -2336,6 +2570,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
2336
2570
  this.errorDetails = errorDetails;
2337
2571
  }
2338
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
+ };
2339
2580
  function createSpeechSdkError(error) {
2340
2581
  const message = error instanceof Error ? error.message : String(error);
2341
2582
  return new AzureTtsSdkError(message);
@@ -2413,6 +2654,149 @@ function createSpeechConfig(config) {
2413
2654
  }
2414
2655
 
2415
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
+ }
2416
2800
  function closeSpeechResources(speechConfig, synthesizer) {
2417
2801
  try {
2418
2802
  synthesizer.close();
@@ -2485,12 +2869,26 @@ async function synthesizeSsml(ssml, config) {
2485
2869
  ...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
2486
2870
  );
2487
2871
  const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
2872
+ const requestId = result.resultId;
2873
+ const addSourceMetadata = (event) => ({
2874
+ ...event,
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] } : {},
2879
+ ...requestId ? { requestId } : {}
2880
+ });
2881
+ const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
2882
+ const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
2883
+ const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
2488
2884
  resolve({
2489
2885
  audioData: result.audioData,
2490
2886
  durationMs,
2491
- ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
2492
- ...visemes.length > 0 ? { visemes } : {},
2493
- ...bookmarks.length > 0 ? { bookmarks } : {}
2887
+ ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
2888
+ ...requestId ? { requestId } : {},
2889
+ ...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
2890
+ ...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
2891
+ ...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
2494
2892
  });
2495
2893
  };
2496
2894
  try {
@@ -2510,30 +2908,1836 @@ async function synthesizeSsml(ssml, config) {
2510
2908
  }
2511
2909
  });
2512
2910
  }
2911
+ async function synthesizeSsmlChunks(chunks, config) {
2912
+ const results = [];
2913
+ const totalChunks = chunks.length;
2914
+ const report = (event) => config.onProgress?.(event);
2915
+ for (const [index, chunk] of chunks.entries()) {
2916
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
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
2925
+ });
2926
+ }
2927
+ for (const [index, chunk] of chunks.entries()) {
2928
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
2929
+ report({
2930
+ currentChunk: index,
2931
+ totalChunks,
2932
+ percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
2933
+ chunkIndex: index,
2934
+ originalTextRange: input.originalTextRange,
2935
+ status: "synthesizing",
2936
+ durationMs: 0
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
+ }
2970
+ }
2971
+ return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
2972
+ }
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
+ }
2987
+ const boundaries = [];
2988
+ const visemes = [];
2989
+ const bookmarks = [];
2990
+ let durationOffset = 0;
2991
+ for (const result of results) {
2992
+ const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
2993
+ for (const boundary of chunkBoundaries) {
2994
+ const textRange = boundary.textRange ?? result.textRange;
2995
+ const originalTextRange = boundary.originalTextRange ?? textRange;
2996
+ const requestId = boundary.requestId ?? result.requestId;
2997
+ boundaries.push({
2998
+ ...boundary,
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 } } : {},
3004
+ ...textRange ? { textRange: { ...textRange } } : {},
3005
+ ...requestId ? { requestId } : {}
3006
+ });
3007
+ }
3008
+ for (const viseme of result.visemes ?? []) {
3009
+ const textRange = viseme.textRange ?? result.textRange;
3010
+ const originalTextRange = viseme.originalTextRange ?? textRange;
3011
+ const requestId = viseme.requestId ?? result.requestId;
3012
+ visemes.push({
3013
+ ...viseme,
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 } } : {},
3019
+ ...textRange ? { textRange: { ...textRange } } : {},
3020
+ ...requestId ? { requestId } : {}
3021
+ });
3022
+ }
3023
+ for (const bookmark of result.bookmarks ?? []) {
3024
+ const textRange = bookmark.textRange ?? result.textRange;
3025
+ const originalTextRange = bookmark.originalTextRange ?? textRange;
3026
+ const requestId = bookmark.requestId ?? result.requestId;
3027
+ bookmarks.push({
3028
+ ...bookmark,
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 } } : {},
3034
+ ...textRange ? { textRange: { ...textRange } } : {},
3035
+ ...requestId ? { requestId } : {}
3036
+ });
3037
+ }
3038
+ durationOffset += Math.max(0, result.durationMs);
3039
+ }
3040
+ return {
3041
+ audioData: audioData.buffer,
3042
+ durationMs: durationOffset,
3043
+ ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
3044
+ ...visemes.length > 0 ? { visemes } : {},
3045
+ ...bookmarks.length > 0 ? { bookmarks } : {},
3046
+ ...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
3047
+ ...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
3048
+ };
3049
+ }
2513
3050
  async function synthesizeSpeech(ssml, config) {
2514
3051
  return (await synthesizeSsml(ssml, config)).audioData;
2515
3052
  }
2516
3053
 
2517
- // packages/azure-tts-client/src/client.ts
2518
- var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
2519
- var _options;
2520
- var AzureTtsClient = class {
2521
- constructor(options) {
2522
- __privateAdd(this, _options);
2523
- __privateSet(this, _options, options);
3054
+ // packages/ssml-core/dist/index.mjs
3055
+ var __typeError2 = (msg) => {
3056
+ throw TypeError(msg);
3057
+ };
3058
+ var __accessCheck2 = (obj, member, msg) => member.has(obj) || __typeError2("Cannot " + msg);
3059
+ var __privateGet2 = (obj, member, getter) => (__accessCheck2(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
3060
+ var __privateAdd2 = (obj, member, value) => member.has(obj) ? __typeError2("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
3061
+ var __privateSet2 = (obj, member, value, setter) => (__accessCheck2(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
3062
+ var SYNTHESIS_NAMESPACE2 = "http://www.w3.org/2001/10/synthesis";
3063
+ var MSTTS_NAMESPACE2 = "http://www.w3.org/2001/mstts";
3064
+ var MAX_NESTING_DEPTH2 = 1e3;
3065
+ var SSML_TAGS2 = {
3066
+ SPEAK: "speak",
3067
+ VOICE: "voice",
3068
+ PROSODY: "prosody",
3069
+ BREAK: "break",
3070
+ EXPRESS_AS: "express-as",
3071
+ EXPRESS_AS_CAMEL: "expressAs",
3072
+ MSTTS_EXPRESS_AS: "mstts:express-as",
3073
+ SAY_AS: "say-as",
3074
+ SAY_AS_CAMEL: "sayAs",
3075
+ PHONEME: "phoneme",
3076
+ EMPHASIS: "emphasis",
3077
+ AUDIO: "audio",
3078
+ SUB: "sub",
3079
+ LANG: "lang",
3080
+ MARK: "mark",
3081
+ BOOKMARK: "bookmark",
3082
+ LEXICON: "lexicon",
3083
+ PARAGRAPH: "p",
3084
+ SENTENCE: "s",
3085
+ WORD: "w",
3086
+ MSTTS_SILENCE: "mstts:silence",
3087
+ SILENCE: "silence",
3088
+ MSTTS_VISEME: "mstts:viseme",
3089
+ VISEME: "viseme",
3090
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
3091
+ MSTTS_DIALOG: "mstts:dialog",
3092
+ MSTTS_TURN: "mstts:turn",
3093
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
3094
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
3095
+ MSTTS_EMBEDDING: "mstts:embedding",
3096
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
3097
+ };
3098
+ var SSML_ATTRS2 = {
3099
+ VERSION: "version",
3100
+ XMLNS: "xmlns",
3101
+ XML_LANG: "xml:lang",
3102
+ LANG: "lang",
3103
+ MSTTS_XMLNS: "xmlns:mstts",
3104
+ NAME: "name",
3105
+ VOICE: "voice",
3106
+ SPEAKER: "speaker",
3107
+ EFFECT: "effect",
3108
+ RATE: "rate",
3109
+ PITCH: "pitch",
3110
+ VOLUME: "volume",
3111
+ CONTOUR: "contour",
3112
+ RANGE: "range",
3113
+ TIME: "time",
3114
+ STRENGTH: "strength",
3115
+ STYLE: "style",
3116
+ STYLE_DEGREE: "styledegree",
3117
+ STYLE_DEGREE_CAMEL: "styleDegree",
3118
+ STYLE_DEGREE_HYPHEN: "style-degree",
3119
+ ROLE: "role",
3120
+ INTERPRET_AS: "interpret-as",
3121
+ FORMAT: "format",
3122
+ DETAIL: "detail",
3123
+ ALPHABET: "alphabet",
3124
+ PH: "ph",
3125
+ LEVEL: "level",
3126
+ SRC: "src",
3127
+ DESC: "desc",
3128
+ CLIP_BEGIN: "clipBegin",
3129
+ CLIP_END: "clipEnd",
3130
+ SPEED: "speed",
3131
+ REPEAT_COUNT: "repeatCount",
3132
+ REPEAT_DURATION: "repeatDuration",
3133
+ SOUND_LEVEL: "soundLevel",
3134
+ ALIAS: "alias",
3135
+ MARK: "mark",
3136
+ URI: "uri",
3137
+ ID: "id",
3138
+ MODEL: "model",
3139
+ PROFILE: "profile",
3140
+ URL: "url",
3141
+ SPEAKER_PROFILE_ID: "speakerProfileId",
3142
+ TYPE: "type",
3143
+ VALUE: "value",
3144
+ FADE_IN: "fadein",
3145
+ FADE_OUT: "fadeout"
3146
+ };
3147
+ var XML_ENTITIES2 = {
3148
+ amp: "&",
3149
+ apos: "'",
3150
+ gt: ">",
3151
+ lt: "<",
3152
+ quot: '"'
3153
+ };
3154
+ function hasOwn2(object, property) {
3155
+ return Object.getOwnPropertyDescriptor(object, property) !== void 0;
3156
+ }
3157
+ function setAttribute2(attributes, name, value) {
3158
+ Object.defineProperty(attributes, name, {
3159
+ configurable: true,
3160
+ enumerable: true,
3161
+ value,
3162
+ writable: true
3163
+ });
3164
+ }
3165
+ function decodeEntity2(entity) {
3166
+ const namedValue = hasOwn2(XML_ENTITIES2, entity) ? XML_ENTITIES2[entity] : void 0;
3167
+ if (namedValue !== void 0) {
3168
+ return namedValue;
2524
3169
  }
2525
- async synthesize(ssml) {
2526
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
2527
- const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
2528
- __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
2529
- const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
2530
- return synthesizeSpeech(ssml, config);
3170
+ const isHexadecimal = entity.startsWith("#x") || entity.startsWith("#X");
3171
+ const isDecimal = entity.startsWith("#");
3172
+ if (!isHexadecimal && !isDecimal) {
3173
+ throw new Error(`Unknown XML entity: &${entity};`);
2531
3174
  }
2532
- async synthesizeSsml(ssml) {
2533
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
2534
- const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
2535
- __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
2536
- return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
3175
+ const digits = entity.slice(isHexadecimal ? 2 : 1);
3176
+ const codePoint = Number.parseInt(digits, isHexadecimal ? 16 : 10);
3177
+ if (!digits || !Number.isInteger(codePoint) || codePoint < 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || codePoint < 32 && ![9, 10, 13].includes(codePoint)) {
3178
+ throw new Error(`Invalid XML character reference: &${entity};`);
3179
+ }
3180
+ return String.fromCodePoint(codePoint);
3181
+ }
3182
+ function decodeXmlEntities2(value) {
3183
+ let result = "";
3184
+ let start = 0;
3185
+ while (true) {
3186
+ const ampersand = value.indexOf("&", start);
3187
+ if (ampersand === -1) {
3188
+ return result + value.slice(start);
3189
+ }
3190
+ result += value.slice(start, ampersand);
3191
+ const semicolon = value.indexOf(";", ampersand + 1);
3192
+ if (semicolon === -1) {
3193
+ throw new Error("Unterminated XML entity reference");
3194
+ }
3195
+ result += decodeEntity2(value.slice(ampersand + 1, semicolon));
3196
+ start = semicolon + 1;
3197
+ }
3198
+ }
3199
+ function isXmlNameStart2(value) {
3200
+ return value !== void 0 && /[A-Za-z_]/.test(value);
3201
+ }
3202
+ function isXmlNameCharacter2(value) {
3203
+ return value !== void 0 && /[A-Za-z0-9_.:-]/.test(value);
3204
+ }
3205
+ function isXmlWhitespace2(value) {
3206
+ return value === " " || value === " " || value === "\r" || value === "\n";
3207
+ }
3208
+ function removeStandardNamespaceAttributes2(attributes) {
3209
+ if (attributes[SSML_ATTRS2.XMLNS] === SYNTHESIS_NAMESPACE2) {
3210
+ delete attributes[SSML_ATTRS2.XMLNS];
3211
+ }
3212
+ if (attributes[SSML_ATTRS2.MSTTS_XMLNS] === MSTTS_NAMESPACE2) {
3213
+ delete attributes[SSML_ATTRS2.MSTTS_XMLNS];
3214
+ }
3215
+ }
3216
+ var _index2;
3217
+ var XmlParser2 = class {
3218
+ constructor(source) {
3219
+ __privateAdd2(this, _index2, 0);
3220
+ this.source = source;
3221
+ }
3222
+ parse() {
3223
+ if (this.source.charCodeAt(0) === 65279) {
3224
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3225
+ }
3226
+ this.skipMisc();
3227
+ if (__privateGet2(this, _index2) >= this.source.length) {
3228
+ this.fail("SSML input is empty");
3229
+ }
3230
+ if (this.source[__privateGet2(this, _index2)] !== "<") {
3231
+ this.fail("SSML input must start with an XML element");
3232
+ }
3233
+ const root = this.parseElement(0);
3234
+ this.skipMisc();
3235
+ if (__privateGet2(this, _index2) !== this.source.length) {
3236
+ this.fail("Unexpected content after the root XML element");
3237
+ }
3238
+ return root;
3239
+ }
3240
+ parseElement(depth) {
3241
+ if (depth > MAX_NESTING_DEPTH2) {
3242
+ this.fail("XML nesting depth exceeds the supported limit");
3243
+ }
3244
+ this.expect("<");
3245
+ if (this.source[__privateGet2(this, _index2)] === "/") {
3246
+ this.fail("Unexpected closing XML element");
3247
+ }
3248
+ const name = this.parseName();
3249
+ const { attributes, selfClosing } = this.parseStartTag();
3250
+ if (selfClosing) {
3251
+ return { name, attributes, children: [] };
3252
+ }
3253
+ const children = [];
3254
+ while (__privateGet2(this, _index2) < this.source.length) {
3255
+ if (this.source.startsWith("</", __privateGet2(this, _index2))) {
3256
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 2);
3257
+ const closingName = this.parseName();
3258
+ this.skipWhitespace();
3259
+ this.expect(">");
3260
+ if (closingName !== name) {
3261
+ this.fail(`Mismatched closing element: expected </${name}> but found </${closingName}>`);
3262
+ }
3263
+ return { name, attributes, children };
3264
+ }
3265
+ if (this.source.startsWith("<!--", __privateGet2(this, _index2))) {
3266
+ this.skipComment();
3267
+ continue;
3268
+ }
3269
+ if (this.source.startsWith("<![CDATA[", __privateGet2(this, _index2))) {
3270
+ this.appendText(children, this.parseCdata());
3271
+ continue;
3272
+ }
3273
+ if (this.source.startsWith("<?", __privateGet2(this, _index2))) {
3274
+ this.skipProcessingInstruction();
3275
+ continue;
3276
+ }
3277
+ if (this.source.startsWith("<!", __privateGet2(this, _index2))) {
3278
+ this.fail("Unsupported XML declaration inside an element");
3279
+ }
3280
+ if (this.source[__privateGet2(this, _index2)] === "<") {
3281
+ children.push(this.parseElement(depth + 1));
3282
+ } else {
3283
+ this.appendText(children, this.parseText());
3284
+ }
3285
+ }
3286
+ this.fail(`Unclosed XML element: <${name}>`);
3287
+ }
3288
+ parseStartTag() {
3289
+ const attributes = {};
3290
+ while (__privateGet2(this, _index2) < this.source.length) {
3291
+ this.skipWhitespace();
3292
+ if (this.source.startsWith("/>", __privateGet2(this, _index2))) {
3293
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 2);
3294
+ return { attributes, selfClosing: true };
3295
+ }
3296
+ if (this.source[__privateGet2(this, _index2)] === ">") {
3297
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3298
+ return { attributes, selfClosing: false };
3299
+ }
3300
+ const name = this.parseName();
3301
+ this.skipWhitespace();
3302
+ this.expect("=");
3303
+ this.skipWhitespace();
3304
+ const quote = this.source[__privateGet2(this, _index2)];
3305
+ if (quote !== '"' && quote !== "'") {
3306
+ this.fail(`XML attribute ${name} must use a quoted value`);
3307
+ }
3308
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3309
+ const valueStart = __privateGet2(this, _index2);
3310
+ while (__privateGet2(this, _index2) < this.source.length && this.source[__privateGet2(this, _index2)] !== quote) {
3311
+ if (this.source[__privateGet2(this, _index2)] === "<") {
3312
+ this.fail(`Invalid "<" in XML attribute ${name}`);
3313
+ }
3314
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3315
+ }
3316
+ if (__privateGet2(this, _index2) >= this.source.length) {
3317
+ this.fail(`Unclosed XML attribute ${name}`);
3318
+ }
3319
+ const value = decodeXmlEntities2(this.source.slice(valueStart, __privateGet2(this, _index2)));
3320
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3321
+ if (hasOwn2(attributes, name)) {
3322
+ this.fail(`Duplicate XML attribute: ${name}`);
3323
+ }
3324
+ setAttribute2(attributes, name, value);
3325
+ }
3326
+ this.fail("Unclosed XML start tag");
3327
+ }
3328
+ parseText() {
3329
+ const start = __privateGet2(this, _index2);
3330
+ while (__privateGet2(this, _index2) < this.source.length && this.source[__privateGet2(this, _index2)] !== "<") {
3331
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3332
+ }
3333
+ const value = this.source.slice(start, __privateGet2(this, _index2));
3334
+ if (value.includes("]]>")) {
3335
+ this.fail("CDATA termination is not valid in ordinary XML text");
3336
+ }
3337
+ return decodeXmlEntities2(value);
3338
+ }
3339
+ parseCdata() {
3340
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<![CDATA[".length);
3341
+ const end = this.source.indexOf("]]>", __privateGet2(this, _index2));
3342
+ if (end === -1) {
3343
+ this.fail("Unclosed XML CDATA section");
3344
+ }
3345
+ const value = this.source.slice(__privateGet2(this, _index2), end);
3346
+ __privateSet2(this, _index2, end + 3);
3347
+ return value;
3348
+ }
3349
+ skipComment() {
3350
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<!--".length);
3351
+ const end = this.source.indexOf("-->", __privateGet2(this, _index2));
3352
+ if (end === -1) {
3353
+ this.fail("Unclosed XML comment");
3354
+ }
3355
+ if (this.source.slice(__privateGet2(this, _index2), end).includes("--")) {
3356
+ this.fail("XML comments cannot contain consecutive hyphens");
3357
+ }
3358
+ __privateSet2(this, _index2, end + 3);
3359
+ }
3360
+ skipProcessingInstruction() {
3361
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<?".length);
3362
+ this.parseName();
3363
+ const end = this.source.indexOf("?>", __privateGet2(this, _index2));
3364
+ if (end === -1) {
3365
+ this.fail("Unclosed XML processing instruction");
3366
+ }
3367
+ __privateSet2(this, _index2, end + 2);
3368
+ }
3369
+ skipMisc() {
3370
+ while (__privateGet2(this, _index2) < this.source.length) {
3371
+ this.skipWhitespace();
3372
+ if (this.source.startsWith("<!--", __privateGet2(this, _index2))) {
3373
+ this.skipComment();
3374
+ continue;
3375
+ }
3376
+ if (this.source.startsWith("<?", __privateGet2(this, _index2))) {
3377
+ this.skipProcessingInstruction();
3378
+ continue;
3379
+ }
3380
+ if (this.source.startsWith("<!DOCTYPE", __privateGet2(this, _index2))) {
3381
+ this.fail("DOCTYPE declarations are not supported");
3382
+ }
3383
+ break;
3384
+ }
3385
+ }
3386
+ parseName() {
3387
+ const first = this.source[__privateGet2(this, _index2)];
3388
+ if (!isXmlNameStart2(first)) {
3389
+ this.fail("Invalid XML name");
3390
+ }
3391
+ const start = __privateGet2(this, _index2);
3392
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3393
+ while (isXmlNameCharacter2(this.source[__privateGet2(this, _index2)])) {
3394
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3395
+ }
3396
+ return this.source.slice(start, __privateGet2(this, _index2));
3397
+ }
3398
+ appendText(children, value) {
3399
+ if (!value) {
3400
+ return;
3401
+ }
3402
+ const previous = children[children.length - 1];
3403
+ if (typeof previous === "string") {
3404
+ children[children.length - 1] = previous + value;
3405
+ } else {
3406
+ children.push(value);
3407
+ }
3408
+ }
3409
+ skipWhitespace() {
3410
+ while (isXmlWhitespace2(this.source[__privateGet2(this, _index2)])) {
3411
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3412
+ }
3413
+ }
3414
+ expect(value) {
3415
+ if (!this.source.startsWith(value, __privateGet2(this, _index2))) {
3416
+ this.fail(`Expected "${value}"`);
3417
+ }
3418
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + value.length);
3419
+ }
3420
+ fail(message) {
3421
+ throw new Error(`${message} at position ${__privateGet2(this, _index2)}`);
3422
+ }
3423
+ };
3424
+ _index2 = /* @__PURE__ */ new WeakMap();
3425
+ function readAttribute2(attributes, ...names) {
3426
+ let found = false;
3427
+ let value;
3428
+ for (const name of names) {
3429
+ if (hasOwn2(attributes, name)) {
3430
+ if (!found) {
3431
+ value = String(attributes[name]);
3432
+ found = true;
3433
+ }
3434
+ delete attributes[name];
3435
+ }
3436
+ }
3437
+ return value;
3438
+ }
3439
+ function getElementAttributes2(node) {
3440
+ const attributes = { ...node.attributes };
3441
+ removeStandardNamespaceAttributes2(attributes);
3442
+ return attributes;
3443
+ }
3444
+ function finishElement2(element, node, attributes) {
3445
+ if (node.children.length > 0) {
3446
+ element.children = node.children.map(convertNode2);
3447
+ }
3448
+ if (Object.keys(attributes).length > 0) {
3449
+ element.attributes = attributes;
3450
+ }
3451
+ return element;
3452
+ }
3453
+ function convertElement2(node) {
3454
+ const attributes = getElementAttributes2(node);
3455
+ switch (node.name) {
3456
+ case SSML_TAGS2.VOICE: {
3457
+ const element = { type: SSML_TAGS2.VOICE };
3458
+ const name = readAttribute2(attributes, SSML_ATTRS2.NAME);
3459
+ const effect = readAttribute2(attributes, SSML_ATTRS2.EFFECT);
3460
+ if (name !== void 0) element.name = name;
3461
+ if (effect !== void 0) element.effect = effect;
3462
+ return finishElement2(element, node, attributes);
3463
+ }
3464
+ case SSML_TAGS2.PROSODY: {
3465
+ const element = { type: SSML_TAGS2.PROSODY };
3466
+ const rate = readAttribute2(attributes, SSML_ATTRS2.RATE);
3467
+ const pitch = readAttribute2(attributes, SSML_ATTRS2.PITCH);
3468
+ const volume = readAttribute2(attributes, SSML_ATTRS2.VOLUME);
3469
+ const contour = readAttribute2(attributes, SSML_ATTRS2.CONTOUR);
3470
+ const range = readAttribute2(attributes, SSML_ATTRS2.RANGE);
3471
+ if (rate !== void 0) element.rate = rate;
3472
+ if (pitch !== void 0) element.pitch = pitch;
3473
+ if (volume !== void 0) element.volume = volume;
3474
+ if (contour !== void 0) element.contour = contour;
3475
+ if (range !== void 0) element.range = range;
3476
+ return finishElement2(element, node, attributes);
3477
+ }
3478
+ case SSML_TAGS2.BREAK: {
3479
+ const element = { type: SSML_TAGS2.BREAK };
3480
+ const time = readAttribute2(attributes, SSML_ATTRS2.TIME);
3481
+ const strength = readAttribute2(attributes, SSML_ATTRS2.STRENGTH);
3482
+ if (time !== void 0) element.time = time;
3483
+ if (strength !== void 0) element.strength = strength;
3484
+ return finishElement2(element, node, attributes);
3485
+ }
3486
+ case SSML_TAGS2.EXPRESS_AS:
3487
+ case SSML_TAGS2.EXPRESS_AS_CAMEL:
3488
+ case SSML_TAGS2.MSTTS_EXPRESS_AS: {
3489
+ const element = { type: node.name };
3490
+ const style = readAttribute2(attributes, SSML_ATTRS2.STYLE);
3491
+ const styleDegree = readAttribute2(
3492
+ attributes,
3493
+ SSML_ATTRS2.STYLE_DEGREE,
3494
+ SSML_ATTRS2.STYLE_DEGREE_CAMEL,
3495
+ SSML_ATTRS2.STYLE_DEGREE_HYPHEN
3496
+ );
3497
+ const role = readAttribute2(attributes, SSML_ATTRS2.ROLE);
3498
+ if (style !== void 0) element.style = style;
3499
+ if (styleDegree !== void 0) element.styleDegree = styleDegree;
3500
+ if (role !== void 0) element.role = role;
3501
+ return finishElement2(element, node, attributes);
3502
+ }
3503
+ case SSML_TAGS2.SAY_AS:
3504
+ case SSML_TAGS2.SAY_AS_CAMEL: {
3505
+ const element = { type: node.name };
3506
+ const interpretAs = readAttribute2(attributes, SSML_ATTRS2.INTERPRET_AS);
3507
+ const format = readAttribute2(attributes, SSML_ATTRS2.FORMAT);
3508
+ const detail = readAttribute2(attributes, SSML_ATTRS2.DETAIL);
3509
+ if (interpretAs !== void 0) element.interpretAs = interpretAs;
3510
+ if (format !== void 0) element.format = format;
3511
+ if (detail !== void 0) element.detail = detail;
3512
+ return finishElement2(element, node, attributes);
3513
+ }
3514
+ case SSML_TAGS2.PHONEME: {
3515
+ const element = { type: SSML_TAGS2.PHONEME };
3516
+ const alphabet = readAttribute2(attributes, SSML_ATTRS2.ALPHABET);
3517
+ const ph = readAttribute2(attributes, SSML_ATTRS2.PH);
3518
+ if (alphabet !== void 0) element.alphabet = alphabet;
3519
+ if (ph !== void 0) element.ph = ph;
3520
+ return finishElement2(element, node, attributes);
3521
+ }
3522
+ case SSML_TAGS2.EMPHASIS: {
3523
+ const element = { type: SSML_TAGS2.EMPHASIS };
3524
+ const level = readAttribute2(attributes, SSML_ATTRS2.LEVEL);
3525
+ if (level !== void 0) element.level = level;
3526
+ return finishElement2(element, node, attributes);
3527
+ }
3528
+ case SSML_TAGS2.AUDIO: {
3529
+ const element = { type: SSML_TAGS2.AUDIO };
3530
+ const src = readAttribute2(attributes, SSML_ATTRS2.SRC);
3531
+ const desc = readAttribute2(attributes, SSML_ATTRS2.DESC);
3532
+ const clipBegin = readAttribute2(attributes, SSML_ATTRS2.CLIP_BEGIN);
3533
+ const clipEnd = readAttribute2(attributes, SSML_ATTRS2.CLIP_END);
3534
+ const speed = readAttribute2(attributes, SSML_ATTRS2.SPEED);
3535
+ const repeatCount = readAttribute2(attributes, SSML_ATTRS2.REPEAT_COUNT);
3536
+ const repeatDuration = readAttribute2(attributes, SSML_ATTRS2.REPEAT_DURATION);
3537
+ const soundLevel = readAttribute2(attributes, SSML_ATTRS2.SOUND_LEVEL);
3538
+ if (src !== void 0) element.src = src;
3539
+ if (desc !== void 0) element.desc = desc;
3540
+ if (clipBegin !== void 0) element.clipBegin = clipBegin;
3541
+ if (clipEnd !== void 0) element.clipEnd = clipEnd;
3542
+ if (speed !== void 0) element.speed = speed;
3543
+ if (repeatCount !== void 0) element.repeatCount = repeatCount;
3544
+ if (repeatDuration !== void 0) element.repeatDuration = repeatDuration;
3545
+ if (soundLevel !== void 0) element.soundLevel = soundLevel;
3546
+ return finishElement2(element, node, attributes);
3547
+ }
3548
+ case SSML_TAGS2.SUB: {
3549
+ const element = { type: SSML_TAGS2.SUB };
3550
+ const alias = readAttribute2(attributes, SSML_ATTRS2.ALIAS);
3551
+ if (alias !== void 0) element.alias = alias;
3552
+ return finishElement2(element, node, attributes);
3553
+ }
3554
+ case SSML_TAGS2.LANG: {
3555
+ const element = { type: SSML_TAGS2.LANG };
3556
+ const lang = readAttribute2(attributes, SSML_ATTRS2.XML_LANG, SSML_ATTRS2.LANG);
3557
+ if (lang !== void 0) element.lang = lang;
3558
+ return finishElement2(element, node, attributes);
3559
+ }
3560
+ case SSML_TAGS2.MARK: {
3561
+ const element = { type: SSML_TAGS2.MARK };
3562
+ const name = readAttribute2(attributes, SSML_ATTRS2.NAME);
3563
+ if (name !== void 0) element.name = name;
3564
+ return finishElement2(element, node, attributes);
3565
+ }
3566
+ case SSML_TAGS2.BOOKMARK: {
3567
+ const element = { type: SSML_TAGS2.BOOKMARK };
3568
+ const mark = readAttribute2(attributes, SSML_ATTRS2.MARK);
3569
+ if (mark !== void 0) element.mark = mark;
3570
+ return finishElement2(element, node, attributes);
3571
+ }
3572
+ case SSML_TAGS2.LEXICON: {
3573
+ const element = { type: SSML_TAGS2.LEXICON };
3574
+ const uri = readAttribute2(attributes, SSML_ATTRS2.URI);
3575
+ if (uri !== void 0) element.uri = uri;
3576
+ return finishElement2(element, node, attributes);
3577
+ }
3578
+ case SSML_TAGS2.PARAGRAPH: {
3579
+ const element = { type: SSML_TAGS2.PARAGRAPH };
3580
+ return finishElement2(element, node, attributes);
3581
+ }
3582
+ case SSML_TAGS2.SENTENCE: {
3583
+ const element = { type: SSML_TAGS2.SENTENCE };
3584
+ return finishElement2(element, node, attributes);
3585
+ }
3586
+ case SSML_TAGS2.WORD: {
3587
+ const element = { type: SSML_TAGS2.WORD };
3588
+ return finishElement2(element, node, attributes);
3589
+ }
3590
+ case SSML_TAGS2.MSTTS_SILENCE:
3591
+ case SSML_TAGS2.SILENCE: {
3592
+ const element = {
3593
+ type: node.name === SSML_TAGS2.MSTTS_SILENCE ? SSML_TAGS2.MSTTS_SILENCE : SSML_TAGS2.SILENCE
3594
+ };
3595
+ const typeValue = readAttribute2(attributes, SSML_ATTRS2.TYPE);
3596
+ const value = readAttribute2(attributes, SSML_ATTRS2.VALUE);
3597
+ if (typeValue !== void 0) element.typeValue = typeValue;
3598
+ if (value !== void 0) element.value = value;
3599
+ return finishElement2(element, node, attributes);
3600
+ }
3601
+ case SSML_TAGS2.MSTTS_VISEME:
3602
+ case SSML_TAGS2.VISEME: {
3603
+ const element = {
3604
+ type: node.name === SSML_TAGS2.MSTTS_VISEME ? SSML_TAGS2.MSTTS_VISEME : SSML_TAGS2.VISEME
3605
+ };
3606
+ const typeValue = readAttribute2(attributes, SSML_ATTRS2.TYPE);
3607
+ if (typeValue !== void 0) element.typeValue = typeValue;
3608
+ return finishElement2(element, node, attributes);
3609
+ }
3610
+ case SSML_TAGS2.MSTTS_AUDIO_DURATION: {
3611
+ const element = { type: SSML_TAGS2.MSTTS_AUDIO_DURATION };
3612
+ const value = readAttribute2(attributes, SSML_ATTRS2.VALUE);
3613
+ if (value !== void 0) element.value = value;
3614
+ return finishElement2(element, node, attributes);
3615
+ }
3616
+ case SSML_TAGS2.MSTTS_DIALOG: {
3617
+ const element = { type: SSML_TAGS2.MSTTS_DIALOG };
3618
+ return finishElement2(element, node, attributes);
3619
+ }
3620
+ case SSML_TAGS2.MSTTS_TURN: {
3621
+ const element = { type: SSML_TAGS2.MSTTS_TURN };
3622
+ const voice = readAttribute2(attributes, SSML_ATTRS2.VOICE);
3623
+ const speaker = readAttribute2(attributes, SSML_ATTRS2.SPEAKER);
3624
+ if (voice !== void 0) element.voice = voice;
3625
+ if (speaker !== void 0) element.speaker = speaker;
3626
+ return finishElement2(element, node, attributes);
3627
+ }
3628
+ case SSML_TAGS2.MSTTS_BACKGROUND_AUDIO: {
3629
+ const element = { type: SSML_TAGS2.MSTTS_BACKGROUND_AUDIO };
3630
+ const src = readAttribute2(attributes, SSML_ATTRS2.SRC);
3631
+ const volume = readAttribute2(attributes, SSML_ATTRS2.VOLUME);
3632
+ const fadeIn = readAttribute2(attributes, SSML_ATTRS2.FADE_IN);
3633
+ const fadeOut = readAttribute2(attributes, SSML_ATTRS2.FADE_OUT);
3634
+ if (src !== void 0) element.src = src;
3635
+ if (volume !== void 0) element.volume = volume;
3636
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
3637
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
3638
+ return finishElement2(element, node, attributes);
3639
+ }
3640
+ case SSML_TAGS2.MSTTS_TTS_EMBEDDING: {
3641
+ const element = { type: SSML_TAGS2.MSTTS_TTS_EMBEDDING };
3642
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3643
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3644
+ return finishElement2(element, node, attributes);
3645
+ }
3646
+ case SSML_TAGS2.MSTTS_EMBEDDING: {
3647
+ const element = { type: SSML_TAGS2.MSTTS_EMBEDDING };
3648
+ const id = readAttribute2(attributes, SSML_ATTRS2.ID);
3649
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3650
+ if (id !== void 0) element.id = id;
3651
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3652
+ return finishElement2(element, node, attributes);
3653
+ }
3654
+ case SSML_TAGS2.MSTTS_VOICE_CONVERSION: {
3655
+ const element = { type: SSML_TAGS2.MSTTS_VOICE_CONVERSION };
3656
+ const url = readAttribute2(attributes, SSML_ATTRS2.URL);
3657
+ const profile = readAttribute2(attributes, SSML_ATTRS2.PROFILE);
3658
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3659
+ if (url !== void 0) element.url = url;
3660
+ if (profile !== void 0) element.profile = profile;
3661
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3662
+ return finishElement2(element, node, attributes);
3663
+ }
3664
+ default: {
3665
+ const element = {
3666
+ name: node.name,
3667
+ type: "custom"
3668
+ };
3669
+ return finishElement2(element, node, attributes);
3670
+ }
3671
+ }
3672
+ }
3673
+ function convertNode2(node) {
3674
+ return typeof node === "string" ? node : convertElement2(node);
3675
+ }
3676
+ function parseSsml2(xmlString) {
3677
+ if (typeof xmlString !== "string") {
3678
+ throw new TypeError("SSML input must be a string");
3679
+ }
3680
+ const root = new XmlParser2(xmlString).parse();
3681
+ if (root.name !== SSML_TAGS2.SPEAK) {
3682
+ throw new Error(`SSML root element must be <${SSML_TAGS2.SPEAK}>, found <${root.name}>`);
3683
+ }
3684
+ const attributes = { ...root.attributes };
3685
+ const version = readAttribute2(attributes, SSML_ATTRS2.VERSION);
3686
+ const lang = readAttribute2(attributes, SSML_ATTRS2.XML_LANG, SSML_ATTRS2.LANG);
3687
+ if (version === void 0) {
3688
+ throw new Error(`SSML <${SSML_TAGS2.SPEAK}> element is missing the "${SSML_ATTRS2.VERSION}" attribute`);
3689
+ }
3690
+ if (lang === void 0) {
3691
+ throw new Error(`SSML <${SSML_TAGS2.SPEAK}> element is missing the "${SSML_ATTRS2.XML_LANG}" attribute`);
3692
+ }
3693
+ removeStandardNamespaceAttributes2(attributes);
3694
+ const document = {
3695
+ children: root.children.map(convertNode2),
3696
+ lang,
3697
+ type: SSML_TAGS2.SPEAK,
3698
+ version
3699
+ };
3700
+ if (Object.keys(attributes).length > 0) {
3701
+ document.attributes = attributes;
3702
+ }
3703
+ return document;
3704
+ }
3705
+ var AZURE_VOICE_DEFINITIONS2 = [
3706
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
3707
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
3708
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
3709
+ {
3710
+ name: "en-US-GuyNeural",
3711
+ locale: "en-US",
3712
+ styles: [
3713
+ "angry",
3714
+ "cheerful",
3715
+ "excited",
3716
+ "friendly",
3717
+ "hopeful",
3718
+ "newscast",
3719
+ "sad",
3720
+ "shouting",
3721
+ "terrified",
3722
+ "unfriendly",
3723
+ "whispering"
3724
+ ]
3725
+ },
3726
+ {
3727
+ name: "en-US-JennyMultilingualNeural",
3728
+ locale: "en-US",
3729
+ styles: [
3730
+ "cheerful",
3731
+ "empathetic",
3732
+ "excited",
3733
+ "friendly",
3734
+ "hopeful",
3735
+ "sad",
3736
+ "shouting",
3737
+ "terrified",
3738
+ "unfriendly",
3739
+ "whispering"
3740
+ ]
3741
+ },
3742
+ {
3743
+ name: "en-US-JennyNeural",
3744
+ locale: "en-US",
3745
+ styles: [
3746
+ "assistant",
3747
+ "chat",
3748
+ "customerservice",
3749
+ "newscast",
3750
+ "cheerful",
3751
+ "empathetic",
3752
+ "excited",
3753
+ "friendly",
3754
+ "hopeful",
3755
+ "sad",
3756
+ "shouting",
3757
+ "terrified",
3758
+ "unfriendly",
3759
+ "whispering"
3760
+ ]
3761
+ },
3762
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
3763
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
3764
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
3765
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
3766
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
3767
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
3768
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
3769
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
3770
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
3771
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
3772
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
3773
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
3774
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
3775
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
3776
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
3777
+ {
3778
+ name: "ru-RU-SvetlanaNeural",
3779
+ locale: "ru-RU",
3780
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
3781
+ },
3782
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
3783
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
3784
+ {
3785
+ name: "zh-CN-XiaoxiaoNeural",
3786
+ locale: "zh-CN",
3787
+ styles: [
3788
+ "assistant",
3789
+ "chat",
3790
+ "customerservice",
3791
+ "newscast",
3792
+ "cheerful",
3793
+ "empathetic",
3794
+ "excited",
3795
+ "friendly",
3796
+ "hopeful",
3797
+ "sad",
3798
+ "terrified",
3799
+ "whispering",
3800
+ "poetry-reading",
3801
+ "sports_commentary",
3802
+ "sports_commentary_excited",
3803
+ "story"
3804
+ ]
3805
+ },
3806
+ {
3807
+ name: "zh-CN-YunxiNeural",
3808
+ locale: "zh-CN",
3809
+ styles: [
3810
+ "narration-relaxed",
3811
+ "embarrassed",
3812
+ "fearful",
3813
+ "sad",
3814
+ "disgruntled",
3815
+ "serious",
3816
+ "angry",
3817
+ "depressed",
3818
+ "chat",
3819
+ "cheerful",
3820
+ "assistant"
3821
+ ]
3822
+ },
3823
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
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
+ }
3888
+ var ALLOWED_BREAK_STRENGTHS2 = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
3889
+ var ALLOWED_SAY_AS2 = /* @__PURE__ */ new Set([
3890
+ "characters",
3891
+ "spell-out",
3892
+ "cardinal",
3893
+ "ordinal",
3894
+ "number",
3895
+ "date",
3896
+ "time",
3897
+ "telephone",
3898
+ "fraction",
3899
+ "address",
3900
+ "name",
3901
+ "currency",
3902
+ "number_digit"
3903
+ ]);
3904
+ var ALLOWED_ROLES2 = /* @__PURE__ */ new Set([
3905
+ "Girl",
3906
+ "Boy",
3907
+ "YoungAdultFemale",
3908
+ "YoungAdultMale",
3909
+ "OlderAdultFemale",
3910
+ "OlderAdultMale",
3911
+ "SeniorFemale",
3912
+ "SeniorMale"
3913
+ ]);
3914
+ var ALLOWED_EMPHASIS_LEVELS2 = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
3915
+ var ALLOWED_SILENCE_TYPES2 = /* @__PURE__ */ new Set([
3916
+ "Leading",
3917
+ "Tailing",
3918
+ "Sentenceboundary",
3919
+ "Comma",
3920
+ "Semicolon",
3921
+ "Enumerationcomma"
3922
+ ]);
3923
+ var ALLOWED_VISEME_TYPES2 = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
3924
+ var DEFAULT_PREVIEW_TAGS2 = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
3925
+ function featureStatusForTag2(name, options) {
3926
+ const tagName = canonicalTagName2(name);
3927
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
3928
+ ([candidate]) => canonicalTagName2(candidate) === tagName
3929
+ )?.[1];
3930
+ if (configured) return configured;
3931
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS2]).some((candidate) => canonicalTagName2(candidate) === tagName))
3932
+ return "preview";
3933
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName2(candidate) === tagName)) return "deprecated";
3934
+ return void 0;
3935
+ }
3936
+ function decodeAttribute2(value) {
3937
+ return value.replace(
3938
+ /&(?:amp|apos|gt|lt|quot);/gi,
3939
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
3940
+ );
3941
+ }
3942
+ function findTagEnd22(source, start) {
3943
+ let quote = "";
3944
+ for (let index = start; index < source.length; index += 1) {
3945
+ const character = source[index];
3946
+ if (quote) {
3947
+ if (character === quote) quote = "";
3948
+ } else if (character === '"' || character === "'") quote = character;
3949
+ else if (character === ">") return index;
3950
+ }
3951
+ return source.length - 1;
3952
+ }
3953
+ function tokenizeElements2(source) {
3954
+ const tokens = [];
3955
+ const openElements = [];
3956
+ let index = 0;
3957
+ while (index < source.length) {
3958
+ const start = source.indexOf("<", index);
3959
+ if (start === -1) break;
3960
+ if (source.startsWith("<!--", start)) {
3961
+ const end2 = source.indexOf("-->", start + 4);
3962
+ index = end2 === -1 ? source.length : end2 + 3;
3963
+ continue;
3964
+ }
3965
+ if (source.startsWith("<![CDATA[", start)) {
3966
+ const end2 = source.indexOf("]]>", start + 9);
3967
+ index = end2 === -1 ? source.length : end2 + 3;
3968
+ continue;
3969
+ }
3970
+ if (source.startsWith("<?", start)) {
3971
+ const end2 = source.indexOf("?>", start + 2);
3972
+ index = end2 === -1 ? source.length : end2 + 2;
3973
+ continue;
3974
+ }
3975
+ const end = findTagEnd22(source, start + 1);
3976
+ const raw = source.slice(start, end + 1);
3977
+ if (raw.startsWith("</")) {
3978
+ openElements.pop();
3979
+ index = end + 1;
3980
+ continue;
3981
+ }
3982
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
3983
+ if (!nameMatch?.[1]) {
3984
+ index = end + 1;
3985
+ continue;
3986
+ }
3987
+ const attributes = /* @__PURE__ */ new Map();
3988
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
3989
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
3990
+ for (const match of attributeSource.matchAll(attributePattern)) {
3991
+ attributes.set(match[1].toLowerCase(), decodeAttribute2(match[3]));
3992
+ }
3993
+ const selfClosing = /\/\s*>$/.test(raw);
3994
+ const parent = openElements[openElements.length - 1];
3995
+ const childElementIndex = parent?.childElementCount;
3996
+ if (parent) parent.childElementCount += 1;
3997
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
3998
+ const tokenName = nameMatch[1];
3999
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
4000
+ tokens.push({
4001
+ attributes,
4002
+ childElementIndex,
4003
+ end,
4004
+ depth: openElements.length + 1,
4005
+ name: tokenName,
4006
+ parentName: parent?.name,
4007
+ parentVoiceName,
4008
+ selfClosing,
4009
+ start
4010
+ });
4011
+ if (!selfClosing) {
4012
+ openElements.push({
4013
+ childElementCount: 0,
4014
+ name: tokenName,
4015
+ voiceName: tokenVoiceName
4016
+ });
4017
+ }
4018
+ index = end + 1;
4019
+ }
4020
+ return tokens;
4021
+ }
4022
+ function location2(source, offset) {
4023
+ const before = source.slice(0, Math.max(0, offset));
4024
+ const line = before.split("\n").length;
4025
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
4026
+ }
4027
+ function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code) {
4028
+ diagnostics.push({
4029
+ ...location2(source, offset),
4030
+ message,
4031
+ severity,
4032
+ source: "ssml-static-validator",
4033
+ ...code ? { code } : {}
4034
+ });
4035
+ }
4036
+ function isSupportedProsodyRate2(value) {
4037
+ const trimmed = value.trim();
4038
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
4039
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
4040
+ if (!multiplier) return false;
4041
+ const numericValue = Number(multiplier[1]);
4042
+ return numericValue >= 0.5 && numericValue <= 2;
4043
+ }
4044
+ function isValidAzureAudioDuration2(value) {
4045
+ const trimmed = value.trim();
4046
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
4047
+ if (numeric) return Number(numeric[1]) > 0;
4048
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
4049
+ if (!clock) return false;
4050
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
4051
+ }
4052
+ function isValidAzureBackgroundAudioDuration2(value) {
4053
+ const match = /^(\d+)$/.exec(value.trim());
4054
+ if (!match) return false;
4055
+ const milliseconds = Number(match[1]);
4056
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
4057
+ }
4058
+ function attr2(token, name) {
4059
+ return token.attributes.get(name.toLowerCase());
4060
+ }
4061
+ var DEFAULT_LANGUAGE_ALIASES2 = {
4062
+ "zh-CN": ["zh-Hans"],
4063
+ "zh-TW": ["zh-Hant"]
4064
+ };
4065
+ function canonicalLanguageTag2(language) {
4066
+ const trimmed = language.trim();
4067
+ if (!trimmed) return "";
4068
+ try {
4069
+ return new Intl.Locale(trimmed).toString().toLowerCase();
4070
+ } catch {
4071
+ return trimmed.toLowerCase();
4072
+ }
4073
+ }
4074
+ function createLanguageNormalizer2(options) {
4075
+ const aliases = /* @__PURE__ */ new Map();
4076
+ const addAliasGroup = (canonical, values) => {
4077
+ const normalizedCanonical = canonicalLanguageTag2(canonical);
4078
+ if (!normalizedCanonical) return;
4079
+ aliases.set(normalizedCanonical, normalizedCanonical);
4080
+ for (const value of values) {
4081
+ const normalizedValue = canonicalLanguageTag2(value);
4082
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
4083
+ }
4084
+ };
4085
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES2)) addAliasGroup(canonical, values);
4086
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
4087
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
4088
+ return (language) => {
4089
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
4090
+ const normalized = canonicalLanguageTag2(customValue);
4091
+ return aliases.get(normalized) ?? normalized;
4092
+ };
4093
+ }
4094
+ function voiceLocalePrefix2(voiceName) {
4095
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
4096
+ if (!match?.groups) return void 0;
4097
+ const tag = `${match.groups.language}-${match.groups.region}`;
4098
+ return {
4099
+ language: match.groups.language.toLowerCase(),
4100
+ region: match.groups.region.toLowerCase(),
4101
+ tag
4102
+ };
4103
+ }
4104
+ function definitionFromStyleMap2(voiceName, styles) {
4105
+ return {
4106
+ name: voiceName,
4107
+ locale: voiceLocalePrefix2(voiceName)?.tag ?? "",
4108
+ styles
4109
+ };
4110
+ }
4111
+ function normalizeVoiceCatalog2(options) {
4112
+ const definitions = /* @__PURE__ */ new Map();
4113
+ for (const definition of AZURE_VOICE_DEFINITIONS2) definitions.set(definition.name.toLowerCase(), definition);
4114
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
4115
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
4116
+ for (const definition of options.customVoiceDefinitions ?? [])
4117
+ definitions.set(definition.name.toLowerCase(), definition);
4118
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
4119
+ const key = voiceName.toLowerCase();
4120
+ const current = definitions.get(key);
4121
+ definitions.set(key, {
4122
+ ...current ?? definitionFromStyleMap2(voiceName, styles),
4123
+ name: current?.name ?? voiceName,
4124
+ styles: styles.map((style) => style.toLowerCase())
4125
+ });
4126
+ }
4127
+ return definitions;
4128
+ }
4129
+ function diagnosticSeverity2(policy) {
4130
+ if (policy === "ignore") return void 0;
4131
+ return policy === "error" ? "error" : "warning";
4132
+ }
4133
+ function languagePart2(language) {
4134
+ try {
4135
+ return new Intl.Locale(language).language.toLowerCase();
4136
+ } catch {
4137
+ return language.split("-")[0]?.toLowerCase() ?? "";
4138
+ }
4139
+ }
4140
+ function definitionMatchesLanguage2(definition, voiceName, language, normalizeLanguage) {
4141
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix2(voiceName)?.tag ?? ""];
4142
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
4143
+ const normalizedLanguage = normalizeLanguage(language);
4144
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
4145
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
4146
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
4147
+ return normalizedLanguage === languagePart2(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart2(candidate) === normalizedLanguage) : false;
4148
+ }
4149
+ function canonicalTagName2(name) {
4150
+ const normalized = name.toLowerCase();
4151
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
4152
+ if (normalized === "sayas") return "say-as";
4153
+ return normalized;
4154
+ }
4155
+ function validateVoiceFeatureMatrix2(token, source, diagnostics, voiceName, definition) {
4156
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
4157
+ return;
4158
+ const tagName = canonicalTagName2(token.name);
4159
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName2));
4160
+ const supportedTags = definition.supportedTags?.map(canonicalTagName2);
4161
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
4162
+ addDiagnostic2(
4163
+ diagnostics,
4164
+ source,
4165
+ token.start,
4166
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
4167
+ "error",
4168
+ "azure-unsupported-tag-for-voice"
4169
+ );
4170
+ }
4171
+ }
4172
+ function validateAudioSource2(token, source, diagnostics, options, elementName3) {
4173
+ const src = attr2(token, "src");
4174
+ if (!src) {
4175
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
4176
+ return;
4177
+ }
4178
+ let parsed;
4179
+ try {
4180
+ parsed = new URL(src);
4181
+ } catch {
4182
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
4183
+ return;
4184
+ }
4185
+ if (parsed.username || parsed.password)
4186
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
4187
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
4188
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
4189
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
4190
+ try {
4191
+ const configured = new URL(allowedOrigin);
4192
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
4193
+ return false;
4194
+ return configured.origin === parsed.origin;
4195
+ } catch {
4196
+ return false;
4197
+ }
4198
+ }) ?? false;
4199
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
4200
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
4201
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
4202
+ addDiagnostic2(
4203
+ diagnostics,
4204
+ source,
4205
+ token.start,
4206
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
4207
+ );
4208
+ }
4209
+ function validateElement2(token, source, diagnostics, voiceName, options, voiceCatalog) {
4210
+ const name = token.name.toLowerCase();
4211
+ const tagStatus = featureStatusForTag2(token.name, options);
4212
+ if (tagStatus === "preview")
4213
+ addDiagnostic2(
4214
+ diagnostics,
4215
+ source,
4216
+ token.start,
4217
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
4218
+ "warning",
4219
+ "azure-preview-tag"
4220
+ );
4221
+ if (tagStatus === "deprecated")
4222
+ addDiagnostic2(
4223
+ diagnostics,
4224
+ source,
4225
+ token.start,
4226
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
4227
+ "info",
4228
+ "azure-deprecated-tag"
4229
+ );
4230
+ if (name === "voice" && !attr2(token, "name")?.trim())
4231
+ addDiagnostic2(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
4232
+ if (name === "break") {
4233
+ const time = attr2(token, "time");
4234
+ const strength = attr2(token, "strength");
4235
+ if (!time && !strength)
4236
+ addDiagnostic2(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
4237
+ if (time && strength)
4238
+ addDiagnostic2(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
4239
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
4240
+ addDiagnostic2(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
4241
+ if (strength && !ALLOWED_BREAK_STRENGTHS2.has(strength))
4242
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
4243
+ }
4244
+ if (name === "prosody") {
4245
+ const rate = attr2(token, "rate");
4246
+ const pitch = attr2(token, "pitch");
4247
+ const volume = attr2(token, "volume");
4248
+ if (rate && !isSupportedProsodyRate2(rate))
4249
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
4250
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
4251
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
4252
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
4253
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
4254
+ }
4255
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
4256
+ const style = attr2(token, "style");
4257
+ if (!style?.trim())
4258
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
4259
+ const degree = attr2(token, "styledegree") ?? attr2(token, "style-degree");
4260
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
4261
+ addDiagnostic2(
4262
+ diagnostics,
4263
+ source,
4264
+ token.start,
4265
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
4266
+ );
4267
+ const role = attr2(token, "role");
4268
+ if (role && !ALLOWED_ROLES2.has(role))
4269
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
4270
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
4271
+ const supportedStyles = definition?.styles;
4272
+ const severity = diagnosticSeverity2(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
4273
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
4274
+ addDiagnostic2(
4275
+ diagnostics,
4276
+ source,
4277
+ token.start,
4278
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
4279
+ severity,
4280
+ "azure-unsupported-style"
4281
+ );
4282
+ if (style && voiceName && !definition && severity)
4283
+ addDiagnostic2(
4284
+ diagnostics,
4285
+ source,
4286
+ token.start,
4287
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
4288
+ severity
4289
+ );
4290
+ }
4291
+ if (name === "say-as" || name === "sayas") {
4292
+ const interpretAs = attr2(token, "interpret-as");
4293
+ if (!interpretAs || !ALLOWED_SAY_AS2.has(interpretAs))
4294
+ addDiagnostic2(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
4295
+ }
4296
+ if (name === "phoneme" && (!attr2(token, "alphabet") || !attr2(token, "ph")))
4297
+ addDiagnostic2(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
4298
+ if (name === "emphasis" && attr2(token, "level") && !ALLOWED_EMPHASIS_LEVELS2.has(attr2(token, "level") ?? ""))
4299
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr2(token, "level")}".`);
4300
+ if (name === "sub" && !attr2(token, "alias")?.trim())
4301
+ addDiagnostic2(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
4302
+ if (name === "lang" && !attr2(token, "xml:lang")?.trim() && !attr2(token, "lang")?.trim())
4303
+ addDiagnostic2(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
4304
+ if (name === "mark" && !attr2(token, "name")?.trim())
4305
+ addDiagnostic2(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
4306
+ if (name === "bookmark" && !attr2(token, "mark")?.trim())
4307
+ addDiagnostic2(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
4308
+ if (name === "lexicon") {
4309
+ const uri = attr2(token, "uri");
4310
+ if (!uri) addDiagnostic2(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
4311
+ else {
4312
+ try {
4313
+ const parsed = new URL(uri);
4314
+ if (parsed.protocol !== "https:")
4315
+ addDiagnostic2(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
4316
+ } catch {
4317
+ addDiagnostic2(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
4318
+ }
4319
+ }
4320
+ }
4321
+ if (name === "mstts:silence") {
4322
+ const type = attr2(token, "type");
4323
+ const value = attr2(token, "value");
4324
+ if (!type || !ALLOWED_SILENCE_TYPES2.has(type))
4325
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
4326
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
4327
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
4328
+ }
4329
+ if (name === "mstts:audioduration") {
4330
+ const value = attr2(token, "value");
4331
+ if (!value || !isValidAzureAudioDuration2(value))
4332
+ addDiagnostic2(
4333
+ diagnostics,
4334
+ source,
4335
+ token.start,
4336
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
4337
+ );
4338
+ if (!token.selfClosing)
4339
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
4340
+ }
4341
+ if (name === "mstts:viseme") {
4342
+ const type = attr2(token, "type");
4343
+ if (!type || !ALLOWED_VISEME_TYPES2.has(type))
4344
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
4345
+ }
4346
+ if (name === "audio") {
4347
+ validateAudioSource2(token, source, diagnostics, options, "audio");
4348
+ }
4349
+ if (name === "mstts:turn") {
4350
+ if (!attr2(token, "voice")?.trim() && !attr2(token, "speaker")?.trim())
4351
+ addDiagnostic2(
4352
+ diagnostics,
4353
+ source,
4354
+ token.start,
4355
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
4356
+ );
4357
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
4358
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
4359
+ }
4360
+ if (name === "mstts:backgroundaudio") {
4361
+ validateAudioSource2(token, source, diagnostics, options, "mstts:backgroundaudio");
4362
+ const volume = attr2(token, "volume");
4363
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
4364
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
4365
+ for (const [attribute, value] of [
4366
+ ["fadein", attr2(token, "fadein")],
4367
+ ["fadeout", attr2(token, "fadeout")]
4368
+ ]) {
4369
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration2(value))
4370
+ addDiagnostic2(
4371
+ diagnostics,
4372
+ source,
4373
+ token.start,
4374
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
4375
+ );
4376
+ }
4377
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
4378
+ addDiagnostic2(
4379
+ diagnostics,
4380
+ source,
4381
+ token.start,
4382
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
4383
+ );
4384
+ if (!token.selfClosing)
4385
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
4386
+ }
4387
+ }
4388
+ function validateAzureSsmlStatic2(ssml, options = {}) {
4389
+ const diagnostics = [];
4390
+ if (typeof ssml !== "string") {
4391
+ return [
4392
+ {
4393
+ line: 1,
4394
+ column: 1,
4395
+ message: "SSML input must be a string",
4396
+ severity: "error",
4397
+ source: "ssml-static-validator"
4398
+ }
4399
+ ];
4400
+ }
4401
+ const maxLength = options.maxLength ?? 1e4;
4402
+ if (ssml.length > maxLength)
4403
+ addDiagnostic2(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
4404
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
4405
+ addDiagnostic2(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
4406
+ }
4407
+ try {
4408
+ parseSsml2(ssml);
4409
+ } catch (error) {
4410
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
4411
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
4412
+ addDiagnostic2(diagnostics, ssml, match ? Number(match[1]) : 0, message);
4413
+ return diagnostics;
4414
+ }
4415
+ const tokens = tokenizeElements2(ssml);
4416
+ if (options.maxXmlDepth !== void 0) {
4417
+ for (const token of tokens) {
4418
+ if (token.depth > options.maxXmlDepth) {
4419
+ addDiagnostic2(
4420
+ diagnostics,
4421
+ ssml,
4422
+ token.start,
4423
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
4424
+ );
4425
+ }
4426
+ }
4427
+ }
4428
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
4429
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
4430
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
4431
+ for (const [index, token] of backgroundAudioTokens.entries()) {
4432
+ if (index > 0)
4433
+ addDiagnostic2(
4434
+ diagnostics,
4435
+ ssml,
4436
+ token.start,
4437
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
4438
+ );
4439
+ }
4440
+ if (!speak || voices.length === 0)
4441
+ addDiagnostic2(
4442
+ diagnostics,
4443
+ ssml,
4444
+ speak?.start ?? 0,
4445
+ "Azure SSML requires at least one <voice> element under <speak>."
4446
+ );
4447
+ const voiceName = voices[0] ? attr2(voices[0], "name") : void 0;
4448
+ const voiceCatalog = normalizeVoiceCatalog2(options);
4449
+ const normalizeLanguage = createLanguageNormalizer2(options);
4450
+ const policySeverity = diagnosticSeverity2(options.unknownVoicePolicy ?? "warn");
4451
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
4452
+ for (const token of voicesToValidate) {
4453
+ const name = attr2(token, "name")?.trim();
4454
+ const language = attr2(token, "xml:lang")?.trim() || (speak ? attr2(speak, "xml:lang")?.trim() : void 0);
4455
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
4456
+ if (name && definition?.status === "preview")
4457
+ addDiagnostic2(
4458
+ diagnostics,
4459
+ ssml,
4460
+ token.start,
4461
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
4462
+ "warning",
4463
+ "azure-preview-voice"
4464
+ );
4465
+ if (name && definition?.status === "deprecated")
4466
+ addDiagnostic2(
4467
+ diagnostics,
4468
+ ssml,
4469
+ token.start,
4470
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
4471
+ "info",
4472
+ "azure-deprecated-voice"
4473
+ );
4474
+ if (name && !definition && policySeverity)
4475
+ addDiagnostic2(
4476
+ diagnostics,
4477
+ ssml,
4478
+ token.start,
4479
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
4480
+ policySeverity,
4481
+ "azure-unknown-voice"
4482
+ );
4483
+ if (name && language && definitionMatchesLanguage2(definition, name, language, normalizeLanguage) === false)
4484
+ addDiagnostic2(
4485
+ diagnostics,
4486
+ ssml,
4487
+ token.start,
4488
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
4489
+ "warning",
4490
+ "azure-locale-mismatch"
4491
+ );
4492
+ }
4493
+ for (const token of tokens) {
4494
+ const tokenName = token.name.toLowerCase();
4495
+ const tokenVoiceName = tokenName === "voice" ? attr2(token, "name")?.trim() : tokenName === "mstts:turn" ? attr2(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
4496
+ validateElement2(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
4497
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
4498
+ validateVoiceFeatureMatrix2(token, ssml, diagnostics, tokenVoiceName, definition);
4499
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
4500
+ addDiagnostic2(
4501
+ diagnostics,
4502
+ ssml,
4503
+ token.start,
4504
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
4505
+ "error",
4506
+ "azure-unsupported-model-for-voice"
4507
+ );
4508
+ }
4509
+ }
4510
+ return diagnostics;
4511
+ }
4512
+ function urlAttributes2(token) {
4513
+ const tag = canonicalTagName2(token.name);
4514
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
4515
+ return attributes.flatMap((attribute) => {
4516
+ const value = attr2(token, attribute);
4517
+ return value === void 0 ? [] : [{ attribute, value }];
4518
+ });
4519
+ }
4520
+ function validateAzureSsml2(ssml, options = {}) {
4521
+ const diagnostics = validateAzureSsmlStatic2(ssml, options);
4522
+ const validator = options.urlValidator ?? options.customUrlValidator;
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
+ });
4532
+ let tokens;
4533
+ try {
4534
+ tokens = tokenizeElements2(ssml);
4535
+ } catch {
4536
+ return diagnostics;
4537
+ }
4538
+ const checks = tokens.flatMap(
4539
+ (token) => urlAttributes2(token).map(async ({ attribute, value }) => {
4540
+ try {
4541
+ const result = await boundedValidator(value, { tag: token.name, attribute });
4542
+ const valid = typeof result === "boolean" ? result : result.valid;
4543
+ if (!valid) {
4544
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
4545
+ addDiagnostic2(
4546
+ diagnostics,
4547
+ ssml,
4548
+ token.start,
4549
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
4550
+ );
4551
+ }
4552
+ } catch (error) {
4553
+ const reason = error instanceof Error ? error.message : String(error);
4554
+ addDiagnostic2(
4555
+ diagnostics,
4556
+ ssml,
4557
+ token.start,
4558
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
4559
+ );
4560
+ }
4561
+ })
4562
+ );
4563
+ return Promise.all(checks).then(() => diagnostics);
4564
+ }
4565
+ var AZURE_VOICE_CATALOG_METADATA2 = {
4566
+ apiVersion: "2025-10-01",
4567
+ generatedAt: "2026-08-28T00:00:00.000Z",
4568
+ regions: [],
4569
+ voiceCount: AZURE_VOICE_DEFINITIONS2.length
4570
+ };
4571
+
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
+ };
4582
+ async function synthesizeSsmlSafe(client, ssml, options = {}) {
4583
+ const validationOptions = options.validation ?? options;
4584
+ const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
4585
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
4586
+ if (errors.length > 0) {
4587
+ return {
4588
+ ok: false,
4589
+ success: false,
4590
+ status: "validation-error",
4591
+ error: {
4592
+ kind: "validation",
4593
+ message: "SSML validation failed; the Azure Speech API was not called.",
4594
+ diagnostics: errors
4595
+ }
4596
+ };
4597
+ }
4598
+ try {
4599
+ return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
4600
+ } catch (error) {
4601
+ const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
4602
+ return { ok: false, success: false, status: "azure-api-error", error: azureError };
4603
+ }
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
+ }
4694
+
4695
+ // packages/azure-tts-client/src/client.ts
4696
+ var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
4697
+ var _options;
4698
+ var AzureTtsClient = class {
4699
+ constructor(options) {
4700
+ __privateAdd(this, _options);
4701
+ __privateSet(this, _options, options);
4702
+ }
4703
+ async synthesize(ssml) {
4704
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4705
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4706
+ __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
4707
+ const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
4708
+ return synthesizeSpeech(ssml, config);
4709
+ }
4710
+ async synthesizeSsml(ssml) {
4711
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4712
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4713
+ __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
4714
+ return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
4715
+ }
4716
+ async synthesizeChunks(chunks, options = {}) {
4717
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4718
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4719
+ return synthesizeSsmlChunks(chunks, {
4720
+ endpoint,
4721
+ region,
4722
+ subscriptionKey,
4723
+ outputFormat,
4724
+ signal,
4725
+ timeoutMs,
4726
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
4727
+ });
4728
+ }
4729
+ async synthesizeSsmlSafe(ssml, options = {}) {
4730
+ return synthesizeSsmlSafe(this, ssml, options);
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);
2537
4741
  }
2538
4742
  };
2539
4743
  _options = new WeakMap();
@@ -2590,6 +4794,9 @@ async function fetchAzureVoiceCatalog(options) {
2590
4794
  const secondaryLocales = stringList(record.SecondaryLocaleList);
2591
4795
  const styles = stringList(record.StyleList);
2592
4796
  const status = normalizeStatus(record.Status);
4797
+ const supportedTags = stringList(record.SupportedTags);
4798
+ const unsupportedTags = stringList(record.UnsupportedTags);
4799
+ const models = stringList(record.Models);
2593
4800
  const merged = {
2594
4801
  name: existing?.name ?? name,
2595
4802
  locale: existing?.locale ?? locale,
@@ -2599,6 +4806,12 @@ async function fetchAzureVoiceCatalog(options) {
2599
4806
  if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
2600
4807
  const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
2601
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;
2602
4815
  if (status) merged.status = status;
2603
4816
  else if (existing?.status) merged.status = existing.status;
2604
4817
  voices.set(key, merged);
@@ -2620,9 +4833,13 @@ async function fetchAzureVoiceCatalog(options) {
2620
4833
  AzureTtsClient,
2621
4834
  AzureTtsError,
2622
4835
  AzureTtsSdkError,
4836
+ ChunkValidationError,
4837
+ UnsupportedMergeFormatError,
2623
4838
  areAzureLanguagesEquivalent,
2624
4839
  buildPartialSsml,
2625
4840
  buildSsml,
4841
+ canMergeAudioFormat,
4842
+ createAzureUrlValidatorRunner,
2626
4843
  extractSsmlText,
2627
4844
  extractSsmlTranslatableText,
2628
4845
  fetchAzureVoiceCatalog,
@@ -2631,11 +4848,17 @@ async function fetchAzureVoiceCatalog(options) {
2631
4848
  getBuiltInVoiceCatalogMetadata,
2632
4849
  isValidAzureAudioDuration,
2633
4850
  mapSsmlTextNodes,
4851
+ mergeAudioBuffers,
4852
+ mergeSynthesisResults,
2634
4853
  normalizeAzureLanguage,
2635
4854
  parseSsml,
4855
+ resolveMergeAudioFormat,
2636
4856
  splitSsmlDocument,
2637
4857
  synthesizeSpeech,
2638
4858
  synthesizeSsml,
4859
+ synthesizeSsmlChunks,
4860
+ synthesizeSsmlChunksSafe,
4861
+ synthesizeSsmlSafe,
2639
4862
  validateAzureSsml,
2640
4863
  validateSsml,
2641
4864
  validateSsmlStructureIntegrity