ssml-builder-js 2.11.0 → 2.13.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
@@ -51,9 +51,14 @@ __export(src_exports, {
51
51
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
52
52
  isValidAzureAudioDuration: () => isValidAzureAudioDuration,
53
53
  mapSsmlTextNodes: () => mapSsmlTextNodes,
54
+ mergeSynthesisResults: () => mergeSynthesisResults,
54
55
  normalizeAzureLanguage: () => normalizeAzureLanguage,
55
56
  parseSsml: () => parseSsml,
57
+ splitSsmlDocument: () => splitSsmlDocument,
56
58
  synthesizeSpeech: () => synthesizeSpeech,
59
+ synthesizeSsml: () => synthesizeSsml,
60
+ synthesizeSsmlChunks: () => synthesizeSsmlChunks,
61
+ synthesizeSsmlSafe: () => synthesizeSsmlSafe,
57
62
  validateAzureSsml: () => validateAzureSsml,
58
63
  validateSsml: () => validateSsml,
59
64
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
@@ -987,6 +992,183 @@ function buildPartialSsml(textOrOptions, context) {
987
992
  return serializePartialSsml(textOrOptions.text, textOrOptions);
988
993
  }
989
994
 
995
+ // packages/ssml-core/src/split.ts
996
+ var DEFAULT_MAX_LENGTH = 1e4;
997
+ function cloneElement(element, children) {
998
+ return { ...element, children };
999
+ }
1000
+ function documentWithChildren(document, children) {
1001
+ return buildSsml({ ...document, children });
1002
+ }
1003
+ function wrapWithContext(node, context) {
1004
+ return context.reduceRight((current, parent) => cloneElement(parent, [current]), node);
1005
+ }
1006
+ function documentWithNode(document, node, context) {
1007
+ return documentWithChildren(document, [wrapWithContext(node, context)]);
1008
+ }
1009
+ function splitTextNode(text, document, context, maxLength) {
1010
+ if (!text) return [text];
1011
+ const parts = [];
1012
+ let start = 0;
1013
+ while (start < text.length) {
1014
+ let end = start + 1;
1015
+ let bestEnd = end;
1016
+ while (end <= text.length) {
1017
+ if (documentWithNode(document, text.slice(start, end), context).length > maxLength) break;
1018
+ bestEnd = end;
1019
+ end += 1;
1020
+ }
1021
+ if (bestEnd === start) {
1022
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1023
+ }
1024
+ const segment = text.slice(start, bestEnd);
1025
+ const boundary = Math.max(segment.lastIndexOf(" "), segment.lastIndexOf("\n"), segment.lastIndexOf(" "));
1026
+ const splitEnd = boundary > 0 ? start + boundary + 1 : bestEnd;
1027
+ parts.push(text.slice(start, splitEnd));
1028
+ start = splitEnd;
1029
+ }
1030
+ return parts;
1031
+ }
1032
+ function splitNode(document, node, maxLength, context = []) {
1033
+ if (typeof node === "string" || node.type === "text") {
1034
+ const value = typeof node === "string" ? node : node.value;
1035
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1036
+ return splitTextNode(value, document, context, maxLength);
1037
+ }
1038
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1039
+ const children = node.children ?? [];
1040
+ if (children.length === 0) {
1041
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1042
+ }
1043
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength, [...context, node]));
1044
+ const parts = [];
1045
+ let group = [];
1046
+ const flush = () => {
1047
+ if (group.length > 0) {
1048
+ parts.push(cloneElement(node, group));
1049
+ group = [];
1050
+ }
1051
+ };
1052
+ for (const child of splitChildren) {
1053
+ const candidate = cloneElement(node, [...group, child]);
1054
+ if (documentWithNode(document, candidate, context).length <= maxLength) {
1055
+ group.push(child);
1056
+ continue;
1057
+ }
1058
+ flush();
1059
+ if (documentWithNode(document, cloneElement(node, [child]), context).length > maxLength) {
1060
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1061
+ }
1062
+ group.push(child);
1063
+ }
1064
+ flush();
1065
+ return parts;
1066
+ }
1067
+ function textFromNode(node) {
1068
+ if (typeof node === "string") return node;
1069
+ if (node.type === "text") return node.value;
1070
+ return (node.children ?? []).map(textFromNode).join("");
1071
+ }
1072
+ function collectMarks(node, marks) {
1073
+ if (typeof node === "string" || node.type === "text") return;
1074
+ if (node.type === "mark" && node.name) marks.push(node.name);
1075
+ if (node.type === "bookmark" && node.mark) marks.push(node.mark);
1076
+ for (const child of node.children ?? []) collectMarks(child, marks);
1077
+ }
1078
+ function collectInheritedContext(nodes) {
1079
+ const context = {};
1080
+ const visit = (node) => {
1081
+ if (typeof node === "string" || node.type === "text") return;
1082
+ if (context.voice === void 0 && node.type === "voice" && node.name) context.voice = node.name;
1083
+ if (context.lang === void 0 && node.type === "lang" && node.lang) context.lang = node.lang;
1084
+ if (context.prosody === void 0 && node.type === "prosody") {
1085
+ const prosody = {};
1086
+ for (const [key, value] of Object.entries(node.attributes ?? {})) prosody[key] = String(value);
1087
+ for (const key of ["rate", "pitch", "volume", "contour", "range"]) {
1088
+ const value = node[key];
1089
+ if (value !== void 0) prosody[key] = String(value);
1090
+ }
1091
+ if (Object.keys(prosody).length > 0) context.prosody = prosody;
1092
+ }
1093
+ for (const child of node.children ?? []) visit(child);
1094
+ };
1095
+ nodes.forEach(visit);
1096
+ return context;
1097
+ }
1098
+ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
1099
+ const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
1100
+ const text = nodes.map(textFromNode).join("");
1101
+ const marks = [];
1102
+ for (const node of nodes) collectMarks(node, marks);
1103
+ const inheritedContext = collectInheritedContext(nodes);
1104
+ if (inheritedContext.lang === void 0 && document.lang) inheritedContext.lang = document.lang;
1105
+ return {
1106
+ chunkIndex,
1107
+ ssml: documentWithChildren(document, chunkNodes),
1108
+ originalTextRange: { start: textStart, end: textStart + text.length },
1109
+ inheritedContext,
1110
+ containedMarks: marks,
1111
+ hasBackgroundAudio: chunkNodes.some(
1112
+ (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1113
+ )
1114
+ };
1115
+ }
1116
+ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
1117
+ const resolvedMaxLength = typeof maxLength === "number" ? maxLength : maxLength.maxLength ?? DEFAULT_MAX_LENGTH;
1118
+ const resolvedOptions = typeof maxLength === "number" ? options : maxLength;
1119
+ if (!Number.isInteger(resolvedMaxLength) || resolvedMaxLength <= 0) {
1120
+ throw new RangeError("maxLength must be a positive integer");
1121
+ }
1122
+ const document = parseSsml(ssml);
1123
+ const backgroundAudio = (document.children ?? []).find(
1124
+ (node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
1125
+ );
1126
+ if (ssml.length <= resolvedMaxLength) {
1127
+ return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
1128
+ }
1129
+ const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
1130
+ const plainDocumentLength = documentWithChildren(document, []).length;
1131
+ const backgroundDocumentLength = backgroundAudio ? documentWithChildren(document, [backgroundAudio]).length : plainDocumentLength;
1132
+ const backgroundOverhead = Math.max(0, backgroundDocumentLength - plainDocumentLength);
1133
+ const contentMaxLength = Math.max(1, resolvedMaxLength - backgroundOverhead);
1134
+ const splitChildren = contentChildren.flatMap((child) => splitNode(document, child, contentMaxLength));
1135
+ const chunks = [];
1136
+ let group = [];
1137
+ for (const child of splitChildren) {
1138
+ const candidate = [...group, child];
1139
+ if (documentWithChildren(document, candidate).length <= contentMaxLength) {
1140
+ group = candidate;
1141
+ continue;
1142
+ }
1143
+ if (group.length > 0) chunks.push(group);
1144
+ group = [child];
1145
+ if (documentWithChildren(document, group).length > contentMaxLength) {
1146
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1147
+ }
1148
+ }
1149
+ if (group.length > 0) chunks.push(group);
1150
+ if (chunks.length === 0) {
1151
+ const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
1152
+ if (result.ssml.length > resolvedMaxLength) {
1153
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1154
+ }
1155
+ return [result];
1156
+ }
1157
+ let textStart = 0;
1158
+ return chunks.map((chunk, chunkIndex) => {
1159
+ const result = createChunk(
1160
+ document,
1161
+ chunk,
1162
+ chunkIndex,
1163
+ textStart,
1164
+ backgroundAudio,
1165
+ resolvedOptions.replicateBackgroundAudio ?? false
1166
+ );
1167
+ textStart = result.originalTextRange.end;
1168
+ return result;
1169
+ });
1170
+ }
1171
+
990
1172
  // packages/ssml-core/src/validation.ts
991
1173
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
992
1174
  function validateSsml(xmlString) {
@@ -1681,6 +1863,7 @@ function tokenizeElements(source) {
1681
1863
  attributes,
1682
1864
  childElementIndex,
1683
1865
  end,
1866
+ depth: openElements.length + 1,
1684
1867
  name: tokenName,
1685
1868
  parentName: parent?.name,
1686
1869
  parentVoiceName,
@@ -1871,13 +2054,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1871
2054
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1872
2055
  return;
1873
2056
  }
2057
+ if (parsed.username || parsed.password)
2058
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1874
2059
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1875
2060
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1876
2061
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1877
2062
  try {
1878
- return new URL(allowedOrigin).origin === parsed.origin;
2063
+ const configured = new URL(allowedOrigin);
2064
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
2065
+ return false;
2066
+ return configured.origin === parsed.origin;
1879
2067
  } catch {
1880
- return allowedOrigin === parsed.origin;
2068
+ return false;
1881
2069
  }
1882
2070
  }) ?? false;
1883
2071
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -2069,7 +2257,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
2069
2257
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
2070
2258
  }
2071
2259
  }
2072
- function validateAzureSsml(ssml, options = {}) {
2260
+ function validateAzureSsmlStatic(ssml, options = {}) {
2073
2261
  const diagnostics = [];
2074
2262
  if (typeof ssml !== "string") {
2075
2263
  return [
@@ -2085,6 +2273,9 @@ function validateAzureSsml(ssml, options = {}) {
2085
2273
  const maxLength = options.maxLength ?? 1e4;
2086
2274
  if (ssml.length > maxLength)
2087
2275
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
2276
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
2277
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
2278
+ }
2088
2279
  try {
2089
2280
  parseSsml(ssml);
2090
2281
  } catch (error) {
@@ -2094,6 +2285,18 @@ function validateAzureSsml(ssml, options = {}) {
2094
2285
  return diagnostics;
2095
2286
  }
2096
2287
  const tokens = tokenizeElements(ssml);
2288
+ if (options.maxXmlDepth !== void 0) {
2289
+ for (const token of tokens) {
2290
+ if (token.depth > options.maxXmlDepth) {
2291
+ addDiagnostic(
2292
+ diagnostics,
2293
+ ssml,
2294
+ token.start,
2295
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
2296
+ );
2297
+ }
2298
+ }
2299
+ }
2097
2300
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2098
2301
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2099
2302
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -2178,6 +2381,51 @@ function validateAzureSsml(ssml, options = {}) {
2178
2381
  }
2179
2382
  return diagnostics;
2180
2383
  }
2384
+ function urlAttributes(token) {
2385
+ const tag = canonicalTagName(token.name);
2386
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
2387
+ return attributes.flatMap((attribute) => {
2388
+ const value = attr(token, attribute);
2389
+ return value === void 0 ? [] : [{ attribute, value }];
2390
+ });
2391
+ }
2392
+ function validateAzureSsml(ssml, options = {}) {
2393
+ const diagnostics = validateAzureSsmlStatic(ssml, options);
2394
+ const validator = options.urlValidator ?? options.customUrlValidator;
2395
+ if (!validator || typeof ssml !== "string") return diagnostics;
2396
+ let tokens;
2397
+ try {
2398
+ tokens = tokenizeElements(ssml);
2399
+ } catch {
2400
+ return diagnostics;
2401
+ }
2402
+ const checks = tokens.flatMap(
2403
+ (token) => urlAttributes(token).map(async ({ attribute, value }) => {
2404
+ try {
2405
+ const result = await validator(value, { tag: token.name, attribute });
2406
+ const valid = typeof result === "boolean" ? result : result.valid;
2407
+ if (!valid) {
2408
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
2409
+ addDiagnostic(
2410
+ diagnostics,
2411
+ ssml,
2412
+ token.start,
2413
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2414
+ );
2415
+ }
2416
+ } catch (error) {
2417
+ const reason = error instanceof Error ? error.message : String(error);
2418
+ addDiagnostic(
2419
+ diagnostics,
2420
+ ssml,
2421
+ token.start,
2422
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2423
+ );
2424
+ }
2425
+ })
2426
+ );
2427
+ return Promise.all(checks).then(() => diagnostics);
2428
+ }
2181
2429
 
2182
2430
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2183
2431
  var AZURE_VOICE_CATALOG_METADATA = {
@@ -2302,7 +2550,8 @@ function closeSpeechResources(speechConfig, synthesizer) {
2302
2550
  } catch {
2303
2551
  }
2304
2552
  }
2305
- async function synthesizeSpeech(ssml, config) {
2553
+ var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
2554
+ async function synthesizeSsml(ssml, config) {
2306
2555
  if (config.signal?.aborted) {
2307
2556
  throw createSpeechSdkError("Speech synthesis was cancelled.");
2308
2557
  }
@@ -2329,6 +2578,22 @@ async function synthesizeSpeech(ssml, config) {
2329
2578
  closeResources();
2330
2579
  reject(createSpeechSdkError(error));
2331
2580
  };
2581
+ const boundaries = [];
2582
+ const visemes = [];
2583
+ const bookmarks = [];
2584
+ synthesizer.wordBoundary = (_sender, event) => {
2585
+ boundaries.push({
2586
+ text: event.text,
2587
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
2588
+ durationMs: ticksToMilliseconds(event.duration)
2589
+ });
2590
+ };
2591
+ synthesizer.visemeReceived = (_sender, event) => {
2592
+ visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
2593
+ };
2594
+ synthesizer.bookmarkReached = (_sender, event) => {
2595
+ bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
2596
+ };
2332
2597
  const cb = (result) => {
2333
2598
  if (settled) return;
2334
2599
  const { reason, errorDetails } = result;
@@ -2340,7 +2605,31 @@ async function synthesizeSpeech(ssml, config) {
2340
2605
  settled = true;
2341
2606
  cleanup();
2342
2607
  closeResources();
2343
- resolve(result.audioData);
2608
+ const eventDurationMs = Math.max(
2609
+ 0,
2610
+ ...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
2611
+ ...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
2612
+ ...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
2613
+ );
2614
+ const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
2615
+ const requestId = result.resultId;
2616
+ const addSourceMetadata = (event) => ({
2617
+ ...event,
2618
+ ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
2619
+ ...requestId ? { requestId } : {}
2620
+ });
2621
+ const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
2622
+ const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
2623
+ const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
2624
+ resolve({
2625
+ audioData: result.audioData,
2626
+ durationMs,
2627
+ ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
2628
+ ...requestId ? { requestId } : {},
2629
+ ...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
2630
+ ...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
2631
+ ...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
2632
+ });
2344
2633
  };
2345
2634
  try {
2346
2635
  if (config.signal) {
@@ -2359,21 +2648,1591 @@ async function synthesizeSpeech(ssml, config) {
2359
2648
  }
2360
2649
  });
2361
2650
  }
2651
+ async function synthesizeSsmlChunks(chunks, config) {
2652
+ const results = [];
2653
+ const totalChunks = chunks.length;
2654
+ for (const [index, chunk] of chunks.entries()) {
2655
+ const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
2656
+ const result = await synthesizeSsml(input.ssml, {
2657
+ ...config,
2658
+ ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
2659
+ onProgress: void 0
2660
+ });
2661
+ results.push(result);
2662
+ config.onProgress?.({
2663
+ currentChunk: index + 1,
2664
+ totalChunks,
2665
+ percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100)
2666
+ });
2667
+ }
2668
+ return mergeSynthesisResults(results);
2669
+ }
2670
+ function mergeSynthesisResults(results) {
2671
+ const audioLength = results.reduce((total, result) => total + result.audioData.byteLength, 0);
2672
+ const audioData = new Uint8Array(audioLength);
2673
+ const boundaries = [];
2674
+ const visemes = [];
2675
+ const bookmarks = [];
2676
+ let byteOffset = 0;
2677
+ let durationOffset = 0;
2678
+ for (const result of results) {
2679
+ audioData.set(new Uint8Array(result.audioData), byteOffset);
2680
+ byteOffset += result.audioData.byteLength;
2681
+ const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
2682
+ for (const boundary of chunkBoundaries) {
2683
+ const textRange = boundary.textRange ?? result.textRange;
2684
+ const requestId = boundary.requestId ?? result.requestId;
2685
+ boundaries.push({
2686
+ ...boundary,
2687
+ audioOffsetMs: boundary.audioOffsetMs + durationOffset,
2688
+ ...textRange ? { textRange: { ...textRange } } : {},
2689
+ ...requestId ? { requestId } : {}
2690
+ });
2691
+ }
2692
+ for (const viseme of result.visemes ?? []) {
2693
+ const textRange = viseme.textRange ?? result.textRange;
2694
+ const requestId = viseme.requestId ?? result.requestId;
2695
+ visemes.push({
2696
+ ...viseme,
2697
+ audioOffsetMs: viseme.audioOffsetMs + durationOffset,
2698
+ ...textRange ? { textRange: { ...textRange } } : {},
2699
+ ...requestId ? { requestId } : {}
2700
+ });
2701
+ }
2702
+ for (const bookmark of result.bookmarks ?? []) {
2703
+ const textRange = bookmark.textRange ?? result.textRange;
2704
+ const requestId = bookmark.requestId ?? result.requestId;
2705
+ bookmarks.push({
2706
+ ...bookmark,
2707
+ audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
2708
+ ...textRange ? { textRange: { ...textRange } } : {},
2709
+ ...requestId ? { requestId } : {}
2710
+ });
2711
+ }
2712
+ durationOffset += Math.max(0, result.durationMs);
2713
+ }
2714
+ return {
2715
+ audioData: audioData.buffer,
2716
+ durationMs: durationOffset,
2717
+ ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
2718
+ ...visemes.length > 0 ? { visemes } : {},
2719
+ ...bookmarks.length > 0 ? { bookmarks } : {},
2720
+ ...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
2721
+ ...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
2722
+ };
2723
+ }
2724
+ async function synthesizeSpeech(ssml, config) {
2725
+ return (await synthesizeSsml(ssml, config)).audioData;
2726
+ }
2362
2727
 
2363
- // packages/azure-tts-client/src/client.ts
2364
- var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
2365
- var _options;
2366
- var AzureTtsClient = class {
2367
- constructor(options) {
2368
- __privateAdd(this, _options);
2369
- __privateSet(this, _options, options);
2728
+ // packages/ssml-core/dist/index.mjs
2729
+ var __typeError2 = (msg) => {
2730
+ throw TypeError(msg);
2731
+ };
2732
+ var __accessCheck2 = (obj, member, msg) => member.has(obj) || __typeError2("Cannot " + msg);
2733
+ var __privateGet2 = (obj, member, getter) => (__accessCheck2(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
2734
+ 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);
2735
+ var __privateSet2 = (obj, member, value, setter) => (__accessCheck2(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
2736
+ var SYNTHESIS_NAMESPACE2 = "http://www.w3.org/2001/10/synthesis";
2737
+ var MSTTS_NAMESPACE2 = "http://www.w3.org/2001/mstts";
2738
+ var MAX_NESTING_DEPTH2 = 1e3;
2739
+ var SSML_TAGS2 = {
2740
+ SPEAK: "speak",
2741
+ VOICE: "voice",
2742
+ PROSODY: "prosody",
2743
+ BREAK: "break",
2744
+ EXPRESS_AS: "express-as",
2745
+ EXPRESS_AS_CAMEL: "expressAs",
2746
+ MSTTS_EXPRESS_AS: "mstts:express-as",
2747
+ SAY_AS: "say-as",
2748
+ SAY_AS_CAMEL: "sayAs",
2749
+ PHONEME: "phoneme",
2750
+ EMPHASIS: "emphasis",
2751
+ AUDIO: "audio",
2752
+ SUB: "sub",
2753
+ LANG: "lang",
2754
+ MARK: "mark",
2755
+ BOOKMARK: "bookmark",
2756
+ LEXICON: "lexicon",
2757
+ PARAGRAPH: "p",
2758
+ SENTENCE: "s",
2759
+ WORD: "w",
2760
+ MSTTS_SILENCE: "mstts:silence",
2761
+ SILENCE: "silence",
2762
+ MSTTS_VISEME: "mstts:viseme",
2763
+ VISEME: "viseme",
2764
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
2765
+ MSTTS_DIALOG: "mstts:dialog",
2766
+ MSTTS_TURN: "mstts:turn",
2767
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
2768
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
2769
+ MSTTS_EMBEDDING: "mstts:embedding",
2770
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
2771
+ };
2772
+ var SSML_ATTRS2 = {
2773
+ VERSION: "version",
2774
+ XMLNS: "xmlns",
2775
+ XML_LANG: "xml:lang",
2776
+ LANG: "lang",
2777
+ MSTTS_XMLNS: "xmlns:mstts",
2778
+ NAME: "name",
2779
+ VOICE: "voice",
2780
+ SPEAKER: "speaker",
2781
+ EFFECT: "effect",
2782
+ RATE: "rate",
2783
+ PITCH: "pitch",
2784
+ VOLUME: "volume",
2785
+ CONTOUR: "contour",
2786
+ RANGE: "range",
2787
+ TIME: "time",
2788
+ STRENGTH: "strength",
2789
+ STYLE: "style",
2790
+ STYLE_DEGREE: "styledegree",
2791
+ STYLE_DEGREE_CAMEL: "styleDegree",
2792
+ STYLE_DEGREE_HYPHEN: "style-degree",
2793
+ ROLE: "role",
2794
+ INTERPRET_AS: "interpret-as",
2795
+ FORMAT: "format",
2796
+ DETAIL: "detail",
2797
+ ALPHABET: "alphabet",
2798
+ PH: "ph",
2799
+ LEVEL: "level",
2800
+ SRC: "src",
2801
+ DESC: "desc",
2802
+ CLIP_BEGIN: "clipBegin",
2803
+ CLIP_END: "clipEnd",
2804
+ SPEED: "speed",
2805
+ REPEAT_COUNT: "repeatCount",
2806
+ REPEAT_DURATION: "repeatDuration",
2807
+ SOUND_LEVEL: "soundLevel",
2808
+ ALIAS: "alias",
2809
+ MARK: "mark",
2810
+ URI: "uri",
2811
+ ID: "id",
2812
+ MODEL: "model",
2813
+ PROFILE: "profile",
2814
+ URL: "url",
2815
+ SPEAKER_PROFILE_ID: "speakerProfileId",
2816
+ TYPE: "type",
2817
+ VALUE: "value",
2818
+ FADE_IN: "fadein",
2819
+ FADE_OUT: "fadeout"
2820
+ };
2821
+ var XML_ENTITIES2 = {
2822
+ amp: "&",
2823
+ apos: "'",
2824
+ gt: ">",
2825
+ lt: "<",
2826
+ quot: '"'
2827
+ };
2828
+ function hasOwn2(object, property) {
2829
+ return Object.getOwnPropertyDescriptor(object, property) !== void 0;
2830
+ }
2831
+ function setAttribute2(attributes, name, value) {
2832
+ Object.defineProperty(attributes, name, {
2833
+ configurable: true,
2834
+ enumerable: true,
2835
+ value,
2836
+ writable: true
2837
+ });
2838
+ }
2839
+ function decodeEntity2(entity) {
2840
+ const namedValue = hasOwn2(XML_ENTITIES2, entity) ? XML_ENTITIES2[entity] : void 0;
2841
+ if (namedValue !== void 0) {
2842
+ return namedValue;
2370
2843
  }
2371
- async synthesize(ssml) {
2372
- const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
2373
- const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
2374
- __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
2375
- const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
2376
- return synthesizeSpeech(ssml, config);
2844
+ const isHexadecimal = entity.startsWith("#x") || entity.startsWith("#X");
2845
+ const isDecimal = entity.startsWith("#");
2846
+ if (!isHexadecimal && !isDecimal) {
2847
+ throw new Error(`Unknown XML entity: &${entity};`);
2848
+ }
2849
+ const digits = entity.slice(isHexadecimal ? 2 : 1);
2850
+ const codePoint = Number.parseInt(digits, isHexadecimal ? 16 : 10);
2851
+ if (!digits || !Number.isInteger(codePoint) || codePoint < 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 || codePoint < 32 && ![9, 10, 13].includes(codePoint)) {
2852
+ throw new Error(`Invalid XML character reference: &${entity};`);
2853
+ }
2854
+ return String.fromCodePoint(codePoint);
2855
+ }
2856
+ function decodeXmlEntities2(value) {
2857
+ let result = "";
2858
+ let start = 0;
2859
+ while (true) {
2860
+ const ampersand = value.indexOf("&", start);
2861
+ if (ampersand === -1) {
2862
+ return result + value.slice(start);
2863
+ }
2864
+ result += value.slice(start, ampersand);
2865
+ const semicolon = value.indexOf(";", ampersand + 1);
2866
+ if (semicolon === -1) {
2867
+ throw new Error("Unterminated XML entity reference");
2868
+ }
2869
+ result += decodeEntity2(value.slice(ampersand + 1, semicolon));
2870
+ start = semicolon + 1;
2871
+ }
2872
+ }
2873
+ function isXmlNameStart2(value) {
2874
+ return value !== void 0 && /[A-Za-z_]/.test(value);
2875
+ }
2876
+ function isXmlNameCharacter2(value) {
2877
+ return value !== void 0 && /[A-Za-z0-9_.:-]/.test(value);
2878
+ }
2879
+ function isXmlWhitespace2(value) {
2880
+ return value === " " || value === " " || value === "\r" || value === "\n";
2881
+ }
2882
+ function removeStandardNamespaceAttributes2(attributes) {
2883
+ if (attributes[SSML_ATTRS2.XMLNS] === SYNTHESIS_NAMESPACE2) {
2884
+ delete attributes[SSML_ATTRS2.XMLNS];
2885
+ }
2886
+ if (attributes[SSML_ATTRS2.MSTTS_XMLNS] === MSTTS_NAMESPACE2) {
2887
+ delete attributes[SSML_ATTRS2.MSTTS_XMLNS];
2888
+ }
2889
+ }
2890
+ var _index2;
2891
+ var XmlParser2 = class {
2892
+ constructor(source) {
2893
+ __privateAdd2(this, _index2, 0);
2894
+ this.source = source;
2895
+ }
2896
+ parse() {
2897
+ if (this.source.charCodeAt(0) === 65279) {
2898
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
2899
+ }
2900
+ this.skipMisc();
2901
+ if (__privateGet2(this, _index2) >= this.source.length) {
2902
+ this.fail("SSML input is empty");
2903
+ }
2904
+ if (this.source[__privateGet2(this, _index2)] !== "<") {
2905
+ this.fail("SSML input must start with an XML element");
2906
+ }
2907
+ const root = this.parseElement(0);
2908
+ this.skipMisc();
2909
+ if (__privateGet2(this, _index2) !== this.source.length) {
2910
+ this.fail("Unexpected content after the root XML element");
2911
+ }
2912
+ return root;
2913
+ }
2914
+ parseElement(depth) {
2915
+ if (depth > MAX_NESTING_DEPTH2) {
2916
+ this.fail("XML nesting depth exceeds the supported limit");
2917
+ }
2918
+ this.expect("<");
2919
+ if (this.source[__privateGet2(this, _index2)] === "/") {
2920
+ this.fail("Unexpected closing XML element");
2921
+ }
2922
+ const name = this.parseName();
2923
+ const { attributes, selfClosing } = this.parseStartTag();
2924
+ if (selfClosing) {
2925
+ return { name, attributes, children: [] };
2926
+ }
2927
+ const children = [];
2928
+ while (__privateGet2(this, _index2) < this.source.length) {
2929
+ if (this.source.startsWith("</", __privateGet2(this, _index2))) {
2930
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 2);
2931
+ const closingName = this.parseName();
2932
+ this.skipWhitespace();
2933
+ this.expect(">");
2934
+ if (closingName !== name) {
2935
+ this.fail(`Mismatched closing element: expected </${name}> but found </${closingName}>`);
2936
+ }
2937
+ return { name, attributes, children };
2938
+ }
2939
+ if (this.source.startsWith("<!--", __privateGet2(this, _index2))) {
2940
+ this.skipComment();
2941
+ continue;
2942
+ }
2943
+ if (this.source.startsWith("<![CDATA[", __privateGet2(this, _index2))) {
2944
+ this.appendText(children, this.parseCdata());
2945
+ continue;
2946
+ }
2947
+ if (this.source.startsWith("<?", __privateGet2(this, _index2))) {
2948
+ this.skipProcessingInstruction();
2949
+ continue;
2950
+ }
2951
+ if (this.source.startsWith("<!", __privateGet2(this, _index2))) {
2952
+ this.fail("Unsupported XML declaration inside an element");
2953
+ }
2954
+ if (this.source[__privateGet2(this, _index2)] === "<") {
2955
+ children.push(this.parseElement(depth + 1));
2956
+ } else {
2957
+ this.appendText(children, this.parseText());
2958
+ }
2959
+ }
2960
+ this.fail(`Unclosed XML element: <${name}>`);
2961
+ }
2962
+ parseStartTag() {
2963
+ const attributes = {};
2964
+ while (__privateGet2(this, _index2) < this.source.length) {
2965
+ this.skipWhitespace();
2966
+ if (this.source.startsWith("/>", __privateGet2(this, _index2))) {
2967
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 2);
2968
+ return { attributes, selfClosing: true };
2969
+ }
2970
+ if (this.source[__privateGet2(this, _index2)] === ">") {
2971
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
2972
+ return { attributes, selfClosing: false };
2973
+ }
2974
+ const name = this.parseName();
2975
+ this.skipWhitespace();
2976
+ this.expect("=");
2977
+ this.skipWhitespace();
2978
+ const quote = this.source[__privateGet2(this, _index2)];
2979
+ if (quote !== '"' && quote !== "'") {
2980
+ this.fail(`XML attribute ${name} must use a quoted value`);
2981
+ }
2982
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
2983
+ const valueStart = __privateGet2(this, _index2);
2984
+ while (__privateGet2(this, _index2) < this.source.length && this.source[__privateGet2(this, _index2)] !== quote) {
2985
+ if (this.source[__privateGet2(this, _index2)] === "<") {
2986
+ this.fail(`Invalid "<" in XML attribute ${name}`);
2987
+ }
2988
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
2989
+ }
2990
+ if (__privateGet2(this, _index2) >= this.source.length) {
2991
+ this.fail(`Unclosed XML attribute ${name}`);
2992
+ }
2993
+ const value = decodeXmlEntities2(this.source.slice(valueStart, __privateGet2(this, _index2)));
2994
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
2995
+ if (hasOwn2(attributes, name)) {
2996
+ this.fail(`Duplicate XML attribute: ${name}`);
2997
+ }
2998
+ setAttribute2(attributes, name, value);
2999
+ }
3000
+ this.fail("Unclosed XML start tag");
3001
+ }
3002
+ parseText() {
3003
+ const start = __privateGet2(this, _index2);
3004
+ while (__privateGet2(this, _index2) < this.source.length && this.source[__privateGet2(this, _index2)] !== "<") {
3005
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3006
+ }
3007
+ const value = this.source.slice(start, __privateGet2(this, _index2));
3008
+ if (value.includes("]]>")) {
3009
+ this.fail("CDATA termination is not valid in ordinary XML text");
3010
+ }
3011
+ return decodeXmlEntities2(value);
3012
+ }
3013
+ parseCdata() {
3014
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<![CDATA[".length);
3015
+ const end = this.source.indexOf("]]>", __privateGet2(this, _index2));
3016
+ if (end === -1) {
3017
+ this.fail("Unclosed XML CDATA section");
3018
+ }
3019
+ const value = this.source.slice(__privateGet2(this, _index2), end);
3020
+ __privateSet2(this, _index2, end + 3);
3021
+ return value;
3022
+ }
3023
+ skipComment() {
3024
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<!--".length);
3025
+ const end = this.source.indexOf("-->", __privateGet2(this, _index2));
3026
+ if (end === -1) {
3027
+ this.fail("Unclosed XML comment");
3028
+ }
3029
+ if (this.source.slice(__privateGet2(this, _index2), end).includes("--")) {
3030
+ this.fail("XML comments cannot contain consecutive hyphens");
3031
+ }
3032
+ __privateSet2(this, _index2, end + 3);
3033
+ }
3034
+ skipProcessingInstruction() {
3035
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + "<?".length);
3036
+ this.parseName();
3037
+ const end = this.source.indexOf("?>", __privateGet2(this, _index2));
3038
+ if (end === -1) {
3039
+ this.fail("Unclosed XML processing instruction");
3040
+ }
3041
+ __privateSet2(this, _index2, end + 2);
3042
+ }
3043
+ skipMisc() {
3044
+ while (__privateGet2(this, _index2) < this.source.length) {
3045
+ this.skipWhitespace();
3046
+ if (this.source.startsWith("<!--", __privateGet2(this, _index2))) {
3047
+ this.skipComment();
3048
+ continue;
3049
+ }
3050
+ if (this.source.startsWith("<?", __privateGet2(this, _index2))) {
3051
+ this.skipProcessingInstruction();
3052
+ continue;
3053
+ }
3054
+ if (this.source.startsWith("<!DOCTYPE", __privateGet2(this, _index2))) {
3055
+ this.fail("DOCTYPE declarations are not supported");
3056
+ }
3057
+ break;
3058
+ }
3059
+ }
3060
+ parseName() {
3061
+ const first = this.source[__privateGet2(this, _index2)];
3062
+ if (!isXmlNameStart2(first)) {
3063
+ this.fail("Invalid XML name");
3064
+ }
3065
+ const start = __privateGet2(this, _index2);
3066
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3067
+ while (isXmlNameCharacter2(this.source[__privateGet2(this, _index2)])) {
3068
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3069
+ }
3070
+ return this.source.slice(start, __privateGet2(this, _index2));
3071
+ }
3072
+ appendText(children, value) {
3073
+ if (!value) {
3074
+ return;
3075
+ }
3076
+ const previous = children[children.length - 1];
3077
+ if (typeof previous === "string") {
3078
+ children[children.length - 1] = previous + value;
3079
+ } else {
3080
+ children.push(value);
3081
+ }
3082
+ }
3083
+ skipWhitespace() {
3084
+ while (isXmlWhitespace2(this.source[__privateGet2(this, _index2)])) {
3085
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + 1);
3086
+ }
3087
+ }
3088
+ expect(value) {
3089
+ if (!this.source.startsWith(value, __privateGet2(this, _index2))) {
3090
+ this.fail(`Expected "${value}"`);
3091
+ }
3092
+ __privateSet2(this, _index2, __privateGet2(this, _index2) + value.length);
3093
+ }
3094
+ fail(message) {
3095
+ throw new Error(`${message} at position ${__privateGet2(this, _index2)}`);
3096
+ }
3097
+ };
3098
+ _index2 = /* @__PURE__ */ new WeakMap();
3099
+ function readAttribute2(attributes, ...names) {
3100
+ let found = false;
3101
+ let value;
3102
+ for (const name of names) {
3103
+ if (hasOwn2(attributes, name)) {
3104
+ if (!found) {
3105
+ value = String(attributes[name]);
3106
+ found = true;
3107
+ }
3108
+ delete attributes[name];
3109
+ }
3110
+ }
3111
+ return value;
3112
+ }
3113
+ function getElementAttributes2(node) {
3114
+ const attributes = { ...node.attributes };
3115
+ removeStandardNamespaceAttributes2(attributes);
3116
+ return attributes;
3117
+ }
3118
+ function finishElement2(element, node, attributes) {
3119
+ if (node.children.length > 0) {
3120
+ element.children = node.children.map(convertNode2);
3121
+ }
3122
+ if (Object.keys(attributes).length > 0) {
3123
+ element.attributes = attributes;
3124
+ }
3125
+ return element;
3126
+ }
3127
+ function convertElement2(node) {
3128
+ const attributes = getElementAttributes2(node);
3129
+ switch (node.name) {
3130
+ case SSML_TAGS2.VOICE: {
3131
+ const element = { type: SSML_TAGS2.VOICE };
3132
+ const name = readAttribute2(attributes, SSML_ATTRS2.NAME);
3133
+ const effect = readAttribute2(attributes, SSML_ATTRS2.EFFECT);
3134
+ if (name !== void 0) element.name = name;
3135
+ if (effect !== void 0) element.effect = effect;
3136
+ return finishElement2(element, node, attributes);
3137
+ }
3138
+ case SSML_TAGS2.PROSODY: {
3139
+ const element = { type: SSML_TAGS2.PROSODY };
3140
+ const rate = readAttribute2(attributes, SSML_ATTRS2.RATE);
3141
+ const pitch = readAttribute2(attributes, SSML_ATTRS2.PITCH);
3142
+ const volume = readAttribute2(attributes, SSML_ATTRS2.VOLUME);
3143
+ const contour = readAttribute2(attributes, SSML_ATTRS2.CONTOUR);
3144
+ const range = readAttribute2(attributes, SSML_ATTRS2.RANGE);
3145
+ if (rate !== void 0) element.rate = rate;
3146
+ if (pitch !== void 0) element.pitch = pitch;
3147
+ if (volume !== void 0) element.volume = volume;
3148
+ if (contour !== void 0) element.contour = contour;
3149
+ if (range !== void 0) element.range = range;
3150
+ return finishElement2(element, node, attributes);
3151
+ }
3152
+ case SSML_TAGS2.BREAK: {
3153
+ const element = { type: SSML_TAGS2.BREAK };
3154
+ const time = readAttribute2(attributes, SSML_ATTRS2.TIME);
3155
+ const strength = readAttribute2(attributes, SSML_ATTRS2.STRENGTH);
3156
+ if (time !== void 0) element.time = time;
3157
+ if (strength !== void 0) element.strength = strength;
3158
+ return finishElement2(element, node, attributes);
3159
+ }
3160
+ case SSML_TAGS2.EXPRESS_AS:
3161
+ case SSML_TAGS2.EXPRESS_AS_CAMEL:
3162
+ case SSML_TAGS2.MSTTS_EXPRESS_AS: {
3163
+ const element = { type: node.name };
3164
+ const style = readAttribute2(attributes, SSML_ATTRS2.STYLE);
3165
+ const styleDegree = readAttribute2(
3166
+ attributes,
3167
+ SSML_ATTRS2.STYLE_DEGREE,
3168
+ SSML_ATTRS2.STYLE_DEGREE_CAMEL,
3169
+ SSML_ATTRS2.STYLE_DEGREE_HYPHEN
3170
+ );
3171
+ const role = readAttribute2(attributes, SSML_ATTRS2.ROLE);
3172
+ if (style !== void 0) element.style = style;
3173
+ if (styleDegree !== void 0) element.styleDegree = styleDegree;
3174
+ if (role !== void 0) element.role = role;
3175
+ return finishElement2(element, node, attributes);
3176
+ }
3177
+ case SSML_TAGS2.SAY_AS:
3178
+ case SSML_TAGS2.SAY_AS_CAMEL: {
3179
+ const element = { type: node.name };
3180
+ const interpretAs = readAttribute2(attributes, SSML_ATTRS2.INTERPRET_AS);
3181
+ const format = readAttribute2(attributes, SSML_ATTRS2.FORMAT);
3182
+ const detail = readAttribute2(attributes, SSML_ATTRS2.DETAIL);
3183
+ if (interpretAs !== void 0) element.interpretAs = interpretAs;
3184
+ if (format !== void 0) element.format = format;
3185
+ if (detail !== void 0) element.detail = detail;
3186
+ return finishElement2(element, node, attributes);
3187
+ }
3188
+ case SSML_TAGS2.PHONEME: {
3189
+ const element = { type: SSML_TAGS2.PHONEME };
3190
+ const alphabet = readAttribute2(attributes, SSML_ATTRS2.ALPHABET);
3191
+ const ph = readAttribute2(attributes, SSML_ATTRS2.PH);
3192
+ if (alphabet !== void 0) element.alphabet = alphabet;
3193
+ if (ph !== void 0) element.ph = ph;
3194
+ return finishElement2(element, node, attributes);
3195
+ }
3196
+ case SSML_TAGS2.EMPHASIS: {
3197
+ const element = { type: SSML_TAGS2.EMPHASIS };
3198
+ const level = readAttribute2(attributes, SSML_ATTRS2.LEVEL);
3199
+ if (level !== void 0) element.level = level;
3200
+ return finishElement2(element, node, attributes);
3201
+ }
3202
+ case SSML_TAGS2.AUDIO: {
3203
+ const element = { type: SSML_TAGS2.AUDIO };
3204
+ const src = readAttribute2(attributes, SSML_ATTRS2.SRC);
3205
+ const desc = readAttribute2(attributes, SSML_ATTRS2.DESC);
3206
+ const clipBegin = readAttribute2(attributes, SSML_ATTRS2.CLIP_BEGIN);
3207
+ const clipEnd = readAttribute2(attributes, SSML_ATTRS2.CLIP_END);
3208
+ const speed = readAttribute2(attributes, SSML_ATTRS2.SPEED);
3209
+ const repeatCount = readAttribute2(attributes, SSML_ATTRS2.REPEAT_COUNT);
3210
+ const repeatDuration = readAttribute2(attributes, SSML_ATTRS2.REPEAT_DURATION);
3211
+ const soundLevel = readAttribute2(attributes, SSML_ATTRS2.SOUND_LEVEL);
3212
+ if (src !== void 0) element.src = src;
3213
+ if (desc !== void 0) element.desc = desc;
3214
+ if (clipBegin !== void 0) element.clipBegin = clipBegin;
3215
+ if (clipEnd !== void 0) element.clipEnd = clipEnd;
3216
+ if (speed !== void 0) element.speed = speed;
3217
+ if (repeatCount !== void 0) element.repeatCount = repeatCount;
3218
+ if (repeatDuration !== void 0) element.repeatDuration = repeatDuration;
3219
+ if (soundLevel !== void 0) element.soundLevel = soundLevel;
3220
+ return finishElement2(element, node, attributes);
3221
+ }
3222
+ case SSML_TAGS2.SUB: {
3223
+ const element = { type: SSML_TAGS2.SUB };
3224
+ const alias = readAttribute2(attributes, SSML_ATTRS2.ALIAS);
3225
+ if (alias !== void 0) element.alias = alias;
3226
+ return finishElement2(element, node, attributes);
3227
+ }
3228
+ case SSML_TAGS2.LANG: {
3229
+ const element = { type: SSML_TAGS2.LANG };
3230
+ const lang = readAttribute2(attributes, SSML_ATTRS2.XML_LANG, SSML_ATTRS2.LANG);
3231
+ if (lang !== void 0) element.lang = lang;
3232
+ return finishElement2(element, node, attributes);
3233
+ }
3234
+ case SSML_TAGS2.MARK: {
3235
+ const element = { type: SSML_TAGS2.MARK };
3236
+ const name = readAttribute2(attributes, SSML_ATTRS2.NAME);
3237
+ if (name !== void 0) element.name = name;
3238
+ return finishElement2(element, node, attributes);
3239
+ }
3240
+ case SSML_TAGS2.BOOKMARK: {
3241
+ const element = { type: SSML_TAGS2.BOOKMARK };
3242
+ const mark = readAttribute2(attributes, SSML_ATTRS2.MARK);
3243
+ if (mark !== void 0) element.mark = mark;
3244
+ return finishElement2(element, node, attributes);
3245
+ }
3246
+ case SSML_TAGS2.LEXICON: {
3247
+ const element = { type: SSML_TAGS2.LEXICON };
3248
+ const uri = readAttribute2(attributes, SSML_ATTRS2.URI);
3249
+ if (uri !== void 0) element.uri = uri;
3250
+ return finishElement2(element, node, attributes);
3251
+ }
3252
+ case SSML_TAGS2.PARAGRAPH: {
3253
+ const element = { type: SSML_TAGS2.PARAGRAPH };
3254
+ return finishElement2(element, node, attributes);
3255
+ }
3256
+ case SSML_TAGS2.SENTENCE: {
3257
+ const element = { type: SSML_TAGS2.SENTENCE };
3258
+ return finishElement2(element, node, attributes);
3259
+ }
3260
+ case SSML_TAGS2.WORD: {
3261
+ const element = { type: SSML_TAGS2.WORD };
3262
+ return finishElement2(element, node, attributes);
3263
+ }
3264
+ case SSML_TAGS2.MSTTS_SILENCE:
3265
+ case SSML_TAGS2.SILENCE: {
3266
+ const element = {
3267
+ type: node.name === SSML_TAGS2.MSTTS_SILENCE ? SSML_TAGS2.MSTTS_SILENCE : SSML_TAGS2.SILENCE
3268
+ };
3269
+ const typeValue = readAttribute2(attributes, SSML_ATTRS2.TYPE);
3270
+ const value = readAttribute2(attributes, SSML_ATTRS2.VALUE);
3271
+ if (typeValue !== void 0) element.typeValue = typeValue;
3272
+ if (value !== void 0) element.value = value;
3273
+ return finishElement2(element, node, attributes);
3274
+ }
3275
+ case SSML_TAGS2.MSTTS_VISEME:
3276
+ case SSML_TAGS2.VISEME: {
3277
+ const element = {
3278
+ type: node.name === SSML_TAGS2.MSTTS_VISEME ? SSML_TAGS2.MSTTS_VISEME : SSML_TAGS2.VISEME
3279
+ };
3280
+ const typeValue = readAttribute2(attributes, SSML_ATTRS2.TYPE);
3281
+ if (typeValue !== void 0) element.typeValue = typeValue;
3282
+ return finishElement2(element, node, attributes);
3283
+ }
3284
+ case SSML_TAGS2.MSTTS_AUDIO_DURATION: {
3285
+ const element = { type: SSML_TAGS2.MSTTS_AUDIO_DURATION };
3286
+ const value = readAttribute2(attributes, SSML_ATTRS2.VALUE);
3287
+ if (value !== void 0) element.value = value;
3288
+ return finishElement2(element, node, attributes);
3289
+ }
3290
+ case SSML_TAGS2.MSTTS_DIALOG: {
3291
+ const element = { type: SSML_TAGS2.MSTTS_DIALOG };
3292
+ return finishElement2(element, node, attributes);
3293
+ }
3294
+ case SSML_TAGS2.MSTTS_TURN: {
3295
+ const element = { type: SSML_TAGS2.MSTTS_TURN };
3296
+ const voice = readAttribute2(attributes, SSML_ATTRS2.VOICE);
3297
+ const speaker = readAttribute2(attributes, SSML_ATTRS2.SPEAKER);
3298
+ if (voice !== void 0) element.voice = voice;
3299
+ if (speaker !== void 0) element.speaker = speaker;
3300
+ return finishElement2(element, node, attributes);
3301
+ }
3302
+ case SSML_TAGS2.MSTTS_BACKGROUND_AUDIO: {
3303
+ const element = { type: SSML_TAGS2.MSTTS_BACKGROUND_AUDIO };
3304
+ const src = readAttribute2(attributes, SSML_ATTRS2.SRC);
3305
+ const volume = readAttribute2(attributes, SSML_ATTRS2.VOLUME);
3306
+ const fadeIn = readAttribute2(attributes, SSML_ATTRS2.FADE_IN);
3307
+ const fadeOut = readAttribute2(attributes, SSML_ATTRS2.FADE_OUT);
3308
+ if (src !== void 0) element.src = src;
3309
+ if (volume !== void 0) element.volume = volume;
3310
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
3311
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
3312
+ return finishElement2(element, node, attributes);
3313
+ }
3314
+ case SSML_TAGS2.MSTTS_TTS_EMBEDDING: {
3315
+ const element = { type: SSML_TAGS2.MSTTS_TTS_EMBEDDING };
3316
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3317
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3318
+ return finishElement2(element, node, attributes);
3319
+ }
3320
+ case SSML_TAGS2.MSTTS_EMBEDDING: {
3321
+ const element = { type: SSML_TAGS2.MSTTS_EMBEDDING };
3322
+ const id = readAttribute2(attributes, SSML_ATTRS2.ID);
3323
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3324
+ if (id !== void 0) element.id = id;
3325
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3326
+ return finishElement2(element, node, attributes);
3327
+ }
3328
+ case SSML_TAGS2.MSTTS_VOICE_CONVERSION: {
3329
+ const element = { type: SSML_TAGS2.MSTTS_VOICE_CONVERSION };
3330
+ const url = readAttribute2(attributes, SSML_ATTRS2.URL);
3331
+ const profile = readAttribute2(attributes, SSML_ATTRS2.PROFILE);
3332
+ const speakerProfileId = readAttribute2(attributes, SSML_ATTRS2.SPEAKER_PROFILE_ID);
3333
+ if (url !== void 0) element.url = url;
3334
+ if (profile !== void 0) element.profile = profile;
3335
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
3336
+ return finishElement2(element, node, attributes);
3337
+ }
3338
+ default: {
3339
+ const element = {
3340
+ name: node.name,
3341
+ type: "custom"
3342
+ };
3343
+ return finishElement2(element, node, attributes);
3344
+ }
3345
+ }
3346
+ }
3347
+ function convertNode2(node) {
3348
+ return typeof node === "string" ? node : convertElement2(node);
3349
+ }
3350
+ function parseSsml2(xmlString) {
3351
+ if (typeof xmlString !== "string") {
3352
+ throw new TypeError("SSML input must be a string");
3353
+ }
3354
+ const root = new XmlParser2(xmlString).parse();
3355
+ if (root.name !== SSML_TAGS2.SPEAK) {
3356
+ throw new Error(`SSML root element must be <${SSML_TAGS2.SPEAK}>, found <${root.name}>`);
3357
+ }
3358
+ const attributes = { ...root.attributes };
3359
+ const version = readAttribute2(attributes, SSML_ATTRS2.VERSION);
3360
+ const lang = readAttribute2(attributes, SSML_ATTRS2.XML_LANG, SSML_ATTRS2.LANG);
3361
+ if (version === void 0) {
3362
+ throw new Error(`SSML <${SSML_TAGS2.SPEAK}> element is missing the "${SSML_ATTRS2.VERSION}" attribute`);
3363
+ }
3364
+ if (lang === void 0) {
3365
+ throw new Error(`SSML <${SSML_TAGS2.SPEAK}> element is missing the "${SSML_ATTRS2.XML_LANG}" attribute`);
3366
+ }
3367
+ removeStandardNamespaceAttributes2(attributes);
3368
+ const document = {
3369
+ children: root.children.map(convertNode2),
3370
+ lang,
3371
+ type: SSML_TAGS2.SPEAK,
3372
+ version
3373
+ };
3374
+ if (Object.keys(attributes).length > 0) {
3375
+ document.attributes = attributes;
3376
+ }
3377
+ return document;
3378
+ }
3379
+ var AZURE_VOICE_DEFINITIONS2 = [
3380
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
3381
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
3382
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
3383
+ {
3384
+ name: "en-US-GuyNeural",
3385
+ locale: "en-US",
3386
+ styles: [
3387
+ "angry",
3388
+ "cheerful",
3389
+ "excited",
3390
+ "friendly",
3391
+ "hopeful",
3392
+ "newscast",
3393
+ "sad",
3394
+ "shouting",
3395
+ "terrified",
3396
+ "unfriendly",
3397
+ "whispering"
3398
+ ]
3399
+ },
3400
+ {
3401
+ name: "en-US-JennyMultilingualNeural",
3402
+ locale: "en-US",
3403
+ styles: [
3404
+ "cheerful",
3405
+ "empathetic",
3406
+ "excited",
3407
+ "friendly",
3408
+ "hopeful",
3409
+ "sad",
3410
+ "shouting",
3411
+ "terrified",
3412
+ "unfriendly",
3413
+ "whispering"
3414
+ ]
3415
+ },
3416
+ {
3417
+ name: "en-US-JennyNeural",
3418
+ locale: "en-US",
3419
+ styles: [
3420
+ "assistant",
3421
+ "chat",
3422
+ "customerservice",
3423
+ "newscast",
3424
+ "cheerful",
3425
+ "empathetic",
3426
+ "excited",
3427
+ "friendly",
3428
+ "hopeful",
3429
+ "sad",
3430
+ "shouting",
3431
+ "terrified",
3432
+ "unfriendly",
3433
+ "whispering"
3434
+ ]
3435
+ },
3436
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
3437
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
3438
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
3439
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
3440
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
3441
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
3442
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
3443
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
3444
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
3445
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
3446
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
3447
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
3448
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
3449
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
3450
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
3451
+ {
3452
+ name: "ru-RU-SvetlanaNeural",
3453
+ locale: "ru-RU",
3454
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
3455
+ },
3456
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
3457
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
3458
+ {
3459
+ name: "zh-CN-XiaoxiaoNeural",
3460
+ locale: "zh-CN",
3461
+ styles: [
3462
+ "assistant",
3463
+ "chat",
3464
+ "customerservice",
3465
+ "newscast",
3466
+ "cheerful",
3467
+ "empathetic",
3468
+ "excited",
3469
+ "friendly",
3470
+ "hopeful",
3471
+ "sad",
3472
+ "terrified",
3473
+ "whispering",
3474
+ "poetry-reading",
3475
+ "sports_commentary",
3476
+ "sports_commentary_excited",
3477
+ "story"
3478
+ ]
3479
+ },
3480
+ {
3481
+ name: "zh-CN-YunxiNeural",
3482
+ locale: "zh-CN",
3483
+ styles: [
3484
+ "narration-relaxed",
3485
+ "embarrassed",
3486
+ "fearful",
3487
+ "sad",
3488
+ "disgruntled",
3489
+ "serious",
3490
+ "angry",
3491
+ "depressed",
3492
+ "chat",
3493
+ "cheerful",
3494
+ "assistant"
3495
+ ]
3496
+ },
3497
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
3498
+ ];
3499
+ var ALLOWED_BREAK_STRENGTHS2 = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
3500
+ var ALLOWED_SAY_AS2 = /* @__PURE__ */ new Set([
3501
+ "characters",
3502
+ "spell-out",
3503
+ "cardinal",
3504
+ "ordinal",
3505
+ "number",
3506
+ "date",
3507
+ "time",
3508
+ "telephone",
3509
+ "fraction",
3510
+ "address",
3511
+ "name",
3512
+ "currency",
3513
+ "number_digit"
3514
+ ]);
3515
+ var ALLOWED_ROLES2 = /* @__PURE__ */ new Set([
3516
+ "Girl",
3517
+ "Boy",
3518
+ "YoungAdultFemale",
3519
+ "YoungAdultMale",
3520
+ "OlderAdultFemale",
3521
+ "OlderAdultMale",
3522
+ "SeniorFemale",
3523
+ "SeniorMale"
3524
+ ]);
3525
+ var ALLOWED_EMPHASIS_LEVELS2 = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
3526
+ var ALLOWED_SILENCE_TYPES2 = /* @__PURE__ */ new Set([
3527
+ "Leading",
3528
+ "Tailing",
3529
+ "Sentenceboundary",
3530
+ "Comma",
3531
+ "Semicolon",
3532
+ "Enumerationcomma"
3533
+ ]);
3534
+ var ALLOWED_VISEME_TYPES2 = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
3535
+ var DEFAULT_PREVIEW_TAGS2 = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
3536
+ function featureStatusForTag2(name, options) {
3537
+ const tagName = canonicalTagName2(name);
3538
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
3539
+ ([candidate]) => canonicalTagName2(candidate) === tagName
3540
+ )?.[1];
3541
+ if (configured) return configured;
3542
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS2]).some((candidate) => canonicalTagName2(candidate) === tagName))
3543
+ return "preview";
3544
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName2(candidate) === tagName)) return "deprecated";
3545
+ return void 0;
3546
+ }
3547
+ function decodeAttribute2(value) {
3548
+ return value.replace(
3549
+ /&(?:amp|apos|gt|lt|quot);/gi,
3550
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
3551
+ );
3552
+ }
3553
+ function findTagEnd22(source, start) {
3554
+ let quote = "";
3555
+ for (let index = start; index < source.length; index += 1) {
3556
+ const character = source[index];
3557
+ if (quote) {
3558
+ if (character === quote) quote = "";
3559
+ } else if (character === '"' || character === "'") quote = character;
3560
+ else if (character === ">") return index;
3561
+ }
3562
+ return source.length - 1;
3563
+ }
3564
+ function tokenizeElements2(source) {
3565
+ const tokens = [];
3566
+ const openElements = [];
3567
+ let index = 0;
3568
+ while (index < source.length) {
3569
+ const start = source.indexOf("<", index);
3570
+ if (start === -1) break;
3571
+ if (source.startsWith("<!--", start)) {
3572
+ const end2 = source.indexOf("-->", start + 4);
3573
+ index = end2 === -1 ? source.length : end2 + 3;
3574
+ continue;
3575
+ }
3576
+ if (source.startsWith("<![CDATA[", start)) {
3577
+ const end2 = source.indexOf("]]>", start + 9);
3578
+ index = end2 === -1 ? source.length : end2 + 3;
3579
+ continue;
3580
+ }
3581
+ if (source.startsWith("<?", start)) {
3582
+ const end2 = source.indexOf("?>", start + 2);
3583
+ index = end2 === -1 ? source.length : end2 + 2;
3584
+ continue;
3585
+ }
3586
+ const end = findTagEnd22(source, start + 1);
3587
+ const raw = source.slice(start, end + 1);
3588
+ if (raw.startsWith("</")) {
3589
+ openElements.pop();
3590
+ index = end + 1;
3591
+ continue;
3592
+ }
3593
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
3594
+ if (!nameMatch?.[1]) {
3595
+ index = end + 1;
3596
+ continue;
3597
+ }
3598
+ const attributes = /* @__PURE__ */ new Map();
3599
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
3600
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
3601
+ for (const match of attributeSource.matchAll(attributePattern)) {
3602
+ attributes.set(match[1].toLowerCase(), decodeAttribute2(match[3]));
3603
+ }
3604
+ const selfClosing = /\/\s*>$/.test(raw);
3605
+ const parent = openElements[openElements.length - 1];
3606
+ const childElementIndex = parent?.childElementCount;
3607
+ if (parent) parent.childElementCount += 1;
3608
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
3609
+ const tokenName = nameMatch[1];
3610
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
3611
+ tokens.push({
3612
+ attributes,
3613
+ childElementIndex,
3614
+ end,
3615
+ depth: openElements.length + 1,
3616
+ name: tokenName,
3617
+ parentName: parent?.name,
3618
+ parentVoiceName,
3619
+ selfClosing,
3620
+ start
3621
+ });
3622
+ if (!selfClosing) {
3623
+ openElements.push({
3624
+ childElementCount: 0,
3625
+ name: tokenName,
3626
+ voiceName: tokenVoiceName
3627
+ });
3628
+ }
3629
+ index = end + 1;
3630
+ }
3631
+ return tokens;
3632
+ }
3633
+ function location2(source, offset) {
3634
+ const before = source.slice(0, Math.max(0, offset));
3635
+ const line = before.split("\n").length;
3636
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
3637
+ }
3638
+ function addDiagnostic2(diagnostics, source, offset, message, severity = "error", code) {
3639
+ diagnostics.push({
3640
+ ...location2(source, offset),
3641
+ message,
3642
+ severity,
3643
+ source: "ssml-static-validator",
3644
+ ...code ? { code } : {}
3645
+ });
3646
+ }
3647
+ function isSupportedProsodyRate2(value) {
3648
+ const trimmed = value.trim();
3649
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
3650
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
3651
+ if (!multiplier) return false;
3652
+ const numericValue = Number(multiplier[1]);
3653
+ return numericValue >= 0.5 && numericValue <= 2;
3654
+ }
3655
+ function isValidAzureAudioDuration2(value) {
3656
+ const trimmed = value.trim();
3657
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
3658
+ if (numeric) return Number(numeric[1]) > 0;
3659
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
3660
+ if (!clock) return false;
3661
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
3662
+ }
3663
+ function isValidAzureBackgroundAudioDuration2(value) {
3664
+ const match = /^(\d+)$/.exec(value.trim());
3665
+ if (!match) return false;
3666
+ const milliseconds = Number(match[1]);
3667
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
3668
+ }
3669
+ function attr2(token, name) {
3670
+ return token.attributes.get(name.toLowerCase());
3671
+ }
3672
+ var DEFAULT_LANGUAGE_ALIASES2 = {
3673
+ "zh-CN": ["zh-Hans"],
3674
+ "zh-TW": ["zh-Hant"]
3675
+ };
3676
+ function canonicalLanguageTag2(language) {
3677
+ const trimmed = language.trim();
3678
+ if (!trimmed) return "";
3679
+ try {
3680
+ return new Intl.Locale(trimmed).toString().toLowerCase();
3681
+ } catch {
3682
+ return trimmed.toLowerCase();
3683
+ }
3684
+ }
3685
+ function createLanguageNormalizer2(options) {
3686
+ const aliases = /* @__PURE__ */ new Map();
3687
+ const addAliasGroup = (canonical, values) => {
3688
+ const normalizedCanonical = canonicalLanguageTag2(canonical);
3689
+ if (!normalizedCanonical) return;
3690
+ aliases.set(normalizedCanonical, normalizedCanonical);
3691
+ for (const value of values) {
3692
+ const normalizedValue = canonicalLanguageTag2(value);
3693
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
3694
+ }
3695
+ };
3696
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES2)) addAliasGroup(canonical, values);
3697
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
3698
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
3699
+ return (language) => {
3700
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
3701
+ const normalized = canonicalLanguageTag2(customValue);
3702
+ return aliases.get(normalized) ?? normalized;
3703
+ };
3704
+ }
3705
+ function voiceLocalePrefix2(voiceName) {
3706
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
3707
+ if (!match?.groups) return void 0;
3708
+ const tag = `${match.groups.language}-${match.groups.region}`;
3709
+ return {
3710
+ language: match.groups.language.toLowerCase(),
3711
+ region: match.groups.region.toLowerCase(),
3712
+ tag
3713
+ };
3714
+ }
3715
+ function definitionFromStyleMap2(voiceName, styles) {
3716
+ return {
3717
+ name: voiceName,
3718
+ locale: voiceLocalePrefix2(voiceName)?.tag ?? "",
3719
+ styles
3720
+ };
3721
+ }
3722
+ function normalizeVoiceCatalog2(options) {
3723
+ const definitions = /* @__PURE__ */ new Map();
3724
+ for (const definition of AZURE_VOICE_DEFINITIONS2) definitions.set(definition.name.toLowerCase(), definition);
3725
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
3726
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
3727
+ for (const definition of options.customVoiceDefinitions ?? [])
3728
+ definitions.set(definition.name.toLowerCase(), definition);
3729
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
3730
+ const key = voiceName.toLowerCase();
3731
+ const current = definitions.get(key);
3732
+ definitions.set(key, {
3733
+ ...current ?? definitionFromStyleMap2(voiceName, styles),
3734
+ name: current?.name ?? voiceName,
3735
+ styles: styles.map((style) => style.toLowerCase())
3736
+ });
3737
+ }
3738
+ return definitions;
3739
+ }
3740
+ function diagnosticSeverity2(policy) {
3741
+ if (policy === "ignore") return void 0;
3742
+ return policy === "error" ? "error" : "warning";
3743
+ }
3744
+ function languagePart2(language) {
3745
+ try {
3746
+ return new Intl.Locale(language).language.toLowerCase();
3747
+ } catch {
3748
+ return language.split("-")[0]?.toLowerCase() ?? "";
3749
+ }
3750
+ }
3751
+ function definitionMatchesLanguage2(definition, voiceName, language, normalizeLanguage) {
3752
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix2(voiceName)?.tag ?? ""];
3753
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
3754
+ const normalizedLanguage = normalizeLanguage(language);
3755
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
3756
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
3757
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
3758
+ return normalizedLanguage === languagePart2(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart2(candidate) === normalizedLanguage) : false;
3759
+ }
3760
+ function canonicalTagName2(name) {
3761
+ const normalized = name.toLowerCase();
3762
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
3763
+ if (normalized === "sayas") return "say-as";
3764
+ return normalized;
3765
+ }
3766
+ function validateVoiceFeatureMatrix2(token, source, diagnostics, voiceName, definition) {
3767
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
3768
+ return;
3769
+ const tagName = canonicalTagName2(token.name);
3770
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName2));
3771
+ const supportedTags = definition.supportedTags?.map(canonicalTagName2);
3772
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
3773
+ addDiagnostic2(
3774
+ diagnostics,
3775
+ source,
3776
+ token.start,
3777
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
3778
+ "error",
3779
+ "azure-unsupported-tag-for-voice"
3780
+ );
3781
+ }
3782
+ }
3783
+ function validateAudioSource2(token, source, diagnostics, options, elementName2) {
3784
+ const src = attr2(token, "src");
3785
+ if (!src) {
3786
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
3787
+ return;
3788
+ }
3789
+ let parsed;
3790
+ try {
3791
+ parsed = new URL(src);
3792
+ } catch {
3793
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
3794
+ return;
3795
+ }
3796
+ if (parsed.username || parsed.password)
3797
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
3798
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
3799
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
3800
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3801
+ try {
3802
+ const configured = new URL(allowedOrigin);
3803
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
3804
+ return false;
3805
+ return configured.origin === parsed.origin;
3806
+ } catch {
3807
+ return false;
3808
+ }
3809
+ }) ?? false;
3810
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
3811
+ addDiagnostic2(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
3812
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
3813
+ addDiagnostic2(
3814
+ diagnostics,
3815
+ source,
3816
+ token.start,
3817
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
3818
+ );
3819
+ }
3820
+ function validateElement2(token, source, diagnostics, voiceName, options, voiceCatalog) {
3821
+ const name = token.name.toLowerCase();
3822
+ const tagStatus = featureStatusForTag2(token.name, options);
3823
+ if (tagStatus === "preview")
3824
+ addDiagnostic2(
3825
+ diagnostics,
3826
+ source,
3827
+ token.start,
3828
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
3829
+ "warning",
3830
+ "azure-preview-tag"
3831
+ );
3832
+ if (tagStatus === "deprecated")
3833
+ addDiagnostic2(
3834
+ diagnostics,
3835
+ source,
3836
+ token.start,
3837
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
3838
+ "info",
3839
+ "azure-deprecated-tag"
3840
+ );
3841
+ if (name === "voice" && !attr2(token, "name")?.trim())
3842
+ addDiagnostic2(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
3843
+ if (name === "break") {
3844
+ const time = attr2(token, "time");
3845
+ const strength = attr2(token, "strength");
3846
+ if (!time && !strength)
3847
+ addDiagnostic2(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
3848
+ if (time && strength)
3849
+ addDiagnostic2(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
3850
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
3851
+ addDiagnostic2(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
3852
+ if (strength && !ALLOWED_BREAK_STRENGTHS2.has(strength))
3853
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
3854
+ }
3855
+ if (name === "prosody") {
3856
+ const rate = attr2(token, "rate");
3857
+ const pitch = attr2(token, "pitch");
3858
+ const volume = attr2(token, "volume");
3859
+ if (rate && !isSupportedProsodyRate2(rate))
3860
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
3861
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
3862
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
3863
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
3864
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
3865
+ }
3866
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
3867
+ const style = attr2(token, "style");
3868
+ if (!style?.trim())
3869
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
3870
+ const degree = attr2(token, "styledegree") ?? attr2(token, "style-degree");
3871
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
3872
+ addDiagnostic2(
3873
+ diagnostics,
3874
+ source,
3875
+ token.start,
3876
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
3877
+ );
3878
+ const role = attr2(token, "role");
3879
+ if (role && !ALLOWED_ROLES2.has(role))
3880
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
3881
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
3882
+ const supportedStyles = definition?.styles;
3883
+ const severity = diagnosticSeverity2(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
3884
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
3885
+ addDiagnostic2(
3886
+ diagnostics,
3887
+ source,
3888
+ token.start,
3889
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
3890
+ severity,
3891
+ "azure-unsupported-style"
3892
+ );
3893
+ if (style && voiceName && !definition && severity)
3894
+ addDiagnostic2(
3895
+ diagnostics,
3896
+ source,
3897
+ token.start,
3898
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
3899
+ severity
3900
+ );
3901
+ }
3902
+ if (name === "say-as" || name === "sayas") {
3903
+ const interpretAs = attr2(token, "interpret-as");
3904
+ if (!interpretAs || !ALLOWED_SAY_AS2.has(interpretAs))
3905
+ addDiagnostic2(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
3906
+ }
3907
+ if (name === "phoneme" && (!attr2(token, "alphabet") || !attr2(token, "ph")))
3908
+ addDiagnostic2(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
3909
+ if (name === "emphasis" && attr2(token, "level") && !ALLOWED_EMPHASIS_LEVELS2.has(attr2(token, "level") ?? ""))
3910
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr2(token, "level")}".`);
3911
+ if (name === "sub" && !attr2(token, "alias")?.trim())
3912
+ addDiagnostic2(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
3913
+ if (name === "lang" && !attr2(token, "xml:lang")?.trim() && !attr2(token, "lang")?.trim())
3914
+ addDiagnostic2(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
3915
+ if (name === "mark" && !attr2(token, "name")?.trim())
3916
+ addDiagnostic2(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
3917
+ if (name === "bookmark" && !attr2(token, "mark")?.trim())
3918
+ addDiagnostic2(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
3919
+ if (name === "lexicon") {
3920
+ const uri = attr2(token, "uri");
3921
+ if (!uri) addDiagnostic2(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
3922
+ else {
3923
+ try {
3924
+ const parsed = new URL(uri);
3925
+ if (parsed.protocol !== "https:")
3926
+ addDiagnostic2(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
3927
+ } catch {
3928
+ addDiagnostic2(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
3929
+ }
3930
+ }
3931
+ }
3932
+ if (name === "mstts:silence") {
3933
+ const type = attr2(token, "type");
3934
+ const value = attr2(token, "value");
3935
+ if (!type || !ALLOWED_SILENCE_TYPES2.has(type))
3936
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
3937
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
3938
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
3939
+ }
3940
+ if (name === "mstts:audioduration") {
3941
+ const value = attr2(token, "value");
3942
+ if (!value || !isValidAzureAudioDuration2(value))
3943
+ addDiagnostic2(
3944
+ diagnostics,
3945
+ source,
3946
+ token.start,
3947
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
3948
+ );
3949
+ if (!token.selfClosing)
3950
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
3951
+ }
3952
+ if (name === "mstts:viseme") {
3953
+ const type = attr2(token, "type");
3954
+ if (!type || !ALLOWED_VISEME_TYPES2.has(type))
3955
+ addDiagnostic2(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
3956
+ }
3957
+ if (name === "audio") {
3958
+ validateAudioSource2(token, source, diagnostics, options, "audio");
3959
+ }
3960
+ if (name === "mstts:turn") {
3961
+ if (!attr2(token, "voice")?.trim() && !attr2(token, "speaker")?.trim())
3962
+ addDiagnostic2(
3963
+ diagnostics,
3964
+ source,
3965
+ token.start,
3966
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
3967
+ );
3968
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
3969
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
3970
+ }
3971
+ if (name === "mstts:backgroundaudio") {
3972
+ validateAudioSource2(token, source, diagnostics, options, "mstts:backgroundaudio");
3973
+ const volume = attr2(token, "volume");
3974
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
3975
+ addDiagnostic2(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
3976
+ for (const [attribute, value] of [
3977
+ ["fadein", attr2(token, "fadein")],
3978
+ ["fadeout", attr2(token, "fadeout")]
3979
+ ]) {
3980
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration2(value))
3981
+ addDiagnostic2(
3982
+ diagnostics,
3983
+ source,
3984
+ token.start,
3985
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
3986
+ );
3987
+ }
3988
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
3989
+ addDiagnostic2(
3990
+ diagnostics,
3991
+ source,
3992
+ token.start,
3993
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
3994
+ );
3995
+ if (!token.selfClosing)
3996
+ addDiagnostic2(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3997
+ }
3998
+ }
3999
+ function validateAzureSsmlStatic2(ssml, options = {}) {
4000
+ const diagnostics = [];
4001
+ if (typeof ssml !== "string") {
4002
+ return [
4003
+ {
4004
+ line: 1,
4005
+ column: 1,
4006
+ message: "SSML input must be a string",
4007
+ severity: "error",
4008
+ source: "ssml-static-validator"
4009
+ }
4010
+ ];
4011
+ }
4012
+ const maxLength = options.maxLength ?? 1e4;
4013
+ if (ssml.length > maxLength)
4014
+ addDiagnostic2(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
4015
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
4016
+ addDiagnostic2(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
4017
+ }
4018
+ try {
4019
+ parseSsml2(ssml);
4020
+ } catch (error) {
4021
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
4022
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
4023
+ addDiagnostic2(diagnostics, ssml, match ? Number(match[1]) : 0, message);
4024
+ return diagnostics;
4025
+ }
4026
+ const tokens = tokenizeElements2(ssml);
4027
+ if (options.maxXmlDepth !== void 0) {
4028
+ for (const token of tokens) {
4029
+ if (token.depth > options.maxXmlDepth) {
4030
+ addDiagnostic2(
4031
+ diagnostics,
4032
+ ssml,
4033
+ token.start,
4034
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
4035
+ );
4036
+ }
4037
+ }
4038
+ }
4039
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
4040
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
4041
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
4042
+ for (const [index, token] of backgroundAudioTokens.entries()) {
4043
+ if (index > 0)
4044
+ addDiagnostic2(
4045
+ diagnostics,
4046
+ ssml,
4047
+ token.start,
4048
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
4049
+ );
4050
+ }
4051
+ if (!speak || voices.length === 0)
4052
+ addDiagnostic2(
4053
+ diagnostics,
4054
+ ssml,
4055
+ speak?.start ?? 0,
4056
+ "Azure SSML requires at least one <voice> element under <speak>."
4057
+ );
4058
+ const voiceName = voices[0] ? attr2(voices[0], "name") : void 0;
4059
+ const voiceCatalog = normalizeVoiceCatalog2(options);
4060
+ const normalizeLanguage = createLanguageNormalizer2(options);
4061
+ const policySeverity = diagnosticSeverity2(options.unknownVoicePolicy ?? "warn");
4062
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
4063
+ for (const token of voicesToValidate) {
4064
+ const name = attr2(token, "name")?.trim();
4065
+ const language = attr2(token, "xml:lang")?.trim() || (speak ? attr2(speak, "xml:lang")?.trim() : void 0);
4066
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
4067
+ if (name && definition?.status === "preview")
4068
+ addDiagnostic2(
4069
+ diagnostics,
4070
+ ssml,
4071
+ token.start,
4072
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
4073
+ "warning",
4074
+ "azure-preview-voice"
4075
+ );
4076
+ if (name && definition?.status === "deprecated")
4077
+ addDiagnostic2(
4078
+ diagnostics,
4079
+ ssml,
4080
+ token.start,
4081
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
4082
+ "info",
4083
+ "azure-deprecated-voice"
4084
+ );
4085
+ if (name && !definition && policySeverity)
4086
+ addDiagnostic2(
4087
+ diagnostics,
4088
+ ssml,
4089
+ token.start,
4090
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
4091
+ policySeverity,
4092
+ "azure-unknown-voice"
4093
+ );
4094
+ if (name && language && definitionMatchesLanguage2(definition, name, language, normalizeLanguage) === false)
4095
+ addDiagnostic2(
4096
+ diagnostics,
4097
+ ssml,
4098
+ token.start,
4099
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
4100
+ "warning",
4101
+ "azure-locale-mismatch"
4102
+ );
4103
+ }
4104
+ for (const token of tokens) {
4105
+ const tokenName = token.name.toLowerCase();
4106
+ const tokenVoiceName = tokenName === "voice" ? attr2(token, "name")?.trim() : tokenName === "mstts:turn" ? attr2(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
4107
+ validateElement2(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
4108
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
4109
+ validateVoiceFeatureMatrix2(token, ssml, diagnostics, tokenVoiceName, definition);
4110
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
4111
+ addDiagnostic2(
4112
+ diagnostics,
4113
+ ssml,
4114
+ token.start,
4115
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
4116
+ "error",
4117
+ "azure-unsupported-model-for-voice"
4118
+ );
4119
+ }
4120
+ }
4121
+ return diagnostics;
4122
+ }
4123
+ function urlAttributes2(token) {
4124
+ const tag = canonicalTagName2(token.name);
4125
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
4126
+ return attributes.flatMap((attribute) => {
4127
+ const value = attr2(token, attribute);
4128
+ return value === void 0 ? [] : [{ attribute, value }];
4129
+ });
4130
+ }
4131
+ function validateAzureSsml2(ssml, options = {}) {
4132
+ const diagnostics = validateAzureSsmlStatic2(ssml, options);
4133
+ const validator = options.urlValidator ?? options.customUrlValidator;
4134
+ if (!validator || typeof ssml !== "string") return diagnostics;
4135
+ let tokens;
4136
+ try {
4137
+ tokens = tokenizeElements2(ssml);
4138
+ } catch {
4139
+ return diagnostics;
4140
+ }
4141
+ const checks = tokens.flatMap(
4142
+ (token) => urlAttributes2(token).map(async ({ attribute, value }) => {
4143
+ try {
4144
+ const result = await validator(value, { tag: token.name, attribute });
4145
+ const valid = typeof result === "boolean" ? result : result.valid;
4146
+ if (!valid) {
4147
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
4148
+ addDiagnostic2(
4149
+ diagnostics,
4150
+ ssml,
4151
+ token.start,
4152
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
4153
+ );
4154
+ }
4155
+ } catch (error) {
4156
+ const reason = error instanceof Error ? error.message : String(error);
4157
+ addDiagnostic2(
4158
+ diagnostics,
4159
+ ssml,
4160
+ token.start,
4161
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
4162
+ );
4163
+ }
4164
+ })
4165
+ );
4166
+ return Promise.all(checks).then(() => diagnostics);
4167
+ }
4168
+ var AZURE_VOICE_CATALOG_METADATA2 = {
4169
+ apiVersion: "2025-10-01",
4170
+ generatedAt: "2026-08-28T00:00:00.000Z",
4171
+ regions: [],
4172
+ voiceCount: AZURE_VOICE_DEFINITIONS2.length
4173
+ };
4174
+
4175
+ // packages/azure-tts-client/src/safe.ts
4176
+ async function synthesizeSsmlSafe(client, ssml, options = {}) {
4177
+ const validationOptions = options.validation ?? options;
4178
+ const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
4179
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
4180
+ if (errors.length > 0) {
4181
+ return {
4182
+ ok: false,
4183
+ success: false,
4184
+ status: "validation-error",
4185
+ error: {
4186
+ kind: "validation",
4187
+ message: "SSML validation failed; the Azure Speech API was not called.",
4188
+ diagnostics: errors
4189
+ }
4190
+ };
4191
+ }
4192
+ try {
4193
+ return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
4194
+ } catch (error) {
4195
+ const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
4196
+ return { ok: false, success: false, status: "azure-api-error", error: azureError };
4197
+ }
4198
+ }
4199
+
4200
+ // packages/azure-tts-client/src/client.ts
4201
+ var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
4202
+ var _options;
4203
+ var AzureTtsClient = class {
4204
+ constructor(options) {
4205
+ __privateAdd(this, _options);
4206
+ __privateSet(this, _options, options);
4207
+ }
4208
+ async synthesize(ssml) {
4209
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4210
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4211
+ __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
4212
+ const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
4213
+ return synthesizeSpeech(ssml, config);
4214
+ }
4215
+ async synthesizeSsml(ssml) {
4216
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4217
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4218
+ __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
4219
+ return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
4220
+ }
4221
+ async synthesizeChunks(chunks, options = {}) {
4222
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
4223
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
4224
+ return synthesizeSsmlChunks(chunks, {
4225
+ endpoint,
4226
+ region,
4227
+ subscriptionKey,
4228
+ outputFormat,
4229
+ signal,
4230
+ timeoutMs,
4231
+ onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
4232
+ });
4233
+ }
4234
+ async synthesizeSsmlSafe(ssml, options = {}) {
4235
+ return synthesizeSsmlSafe(this, ssml, options);
2377
4236
  }
2378
4237
  };
2379
4238
  _options = new WeakMap();
@@ -2471,9 +4330,14 @@ async function fetchAzureVoiceCatalog(options) {
2471
4330
  getBuiltInVoiceCatalogMetadata,
2472
4331
  isValidAzureAudioDuration,
2473
4332
  mapSsmlTextNodes,
4333
+ mergeSynthesisResults,
2474
4334
  normalizeAzureLanguage,
2475
4335
  parseSsml,
4336
+ splitSsmlDocument,
2476
4337
  synthesizeSpeech,
4338
+ synthesizeSsml,
4339
+ synthesizeSsmlChunks,
4340
+ synthesizeSsmlSafe,
2477
4341
  validateAzureSsml,
2478
4342
  validateSsml,
2479
4343
  validateSsmlStructureIntegrity