ssml-builder-js 2.8.0 → 2.9.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.
@@ -213,6 +213,13 @@ var SSML_HOVER_COPY = {
213
213
  parameters: {
214
214
  type: { title: "type", description: "\u30D3\u30BC\u30FC\u30E0\u30A4\u30D9\u30F3\u30C8\u306E\u5F62\u5F0F\u3002" }
215
215
  }
216
+ },
217
+ "mstts:audioduration": {
218
+ title: "\u97F3\u58F0\u9577",
219
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
220
+ parameters: {
221
+ value: { title: "value", description: "\u76EE\u6A19\u6642\u9593\u3002\u4F8B: `10s`\u3001`5000ms`\u3001`00:00:10`\u3002" }
222
+ }
216
223
  }
217
224
  }
218
225
  },
@@ -370,6 +377,13 @@ var SSML_HOVER_COPY = {
370
377
  parameters: {
371
378
  type: { title: "type", description: "The viseme event format." }
372
379
  }
380
+ },
381
+ "mstts:audioduration": {
382
+ title: "Audio duration",
383
+ description: "Sets the target duration of synthesized audio.",
384
+ parameters: {
385
+ value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." }
386
+ }
373
387
  }
374
388
  }
375
389
  }
@@ -526,6 +540,8 @@ function findSsmlVoiceContext(source, offset) {
526
540
  // packages/ssml-editor-react/src/constants/ssmlPresets.ts
527
541
  var ssmlPresets_exports = {};
528
542
  __export(ssmlPresets_exports, {
543
+ AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
544
+ AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
529
545
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
530
546
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
531
547
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -558,6 +574,103 @@ __export(ssmlPresets_exports, {
558
574
  getExpressAsStyleCategory: () => getExpressAsStyleCategory,
559
575
  resolveExpressAsStyles: () => resolveExpressAsStyles
560
576
  });
577
+
578
+ // packages/ssml-editor-react/src/constants/azureVoiceStyleMap.generated.ts
579
+ var AZURE_VOICE_STYLE_MAP = {
580
+ "de-DE-ConradNeural": ["cheerful", "sad"],
581
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
582
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
583
+ "en-US-GuyNeural": [
584
+ "angry",
585
+ "cheerful",
586
+ "excited",
587
+ "friendly",
588
+ "hopeful",
589
+ "newscast",
590
+ "sad",
591
+ "shouting",
592
+ "terrified",
593
+ "unfriendly",
594
+ "whispering"
595
+ ],
596
+ "en-US-JennyMultilingualNeural": [
597
+ "cheerful",
598
+ "empathetic",
599
+ "excited",
600
+ "friendly",
601
+ "hopeful",
602
+ "sad",
603
+ "shouting",
604
+ "terrified",
605
+ "unfriendly",
606
+ "whispering"
607
+ ],
608
+ "en-US-JennyNeural": [
609
+ "assistant",
610
+ "chat",
611
+ "customerservice",
612
+ "newscast",
613
+ "cheerful",
614
+ "empathetic",
615
+ "excited",
616
+ "friendly",
617
+ "hopeful",
618
+ "sad",
619
+ "shouting",
620
+ "terrified",
621
+ "unfriendly",
622
+ "whispering"
623
+ ],
624
+ "es-ES-ElviraNeural": [],
625
+ "fil-PH-AngeloNeural": [],
626
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
627
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
628
+ "id-ID-GadisNeural": [],
629
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
630
+ "ja-JP-KeitaNeural": ["chat"],
631
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
632
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
633
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
634
+ "ms-MY-YasminNeural": [],
635
+ "pt-BR-FranciscaNeural": ["calm"],
636
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
637
+ "th-TH-PremwadeeNeural": [],
638
+ "vi-VN-HoaiMyNeural": [],
639
+ "zh-CN-XiaoxiaoNeural": [
640
+ "assistant",
641
+ "chat",
642
+ "customerservice",
643
+ "newscast",
644
+ "cheerful",
645
+ "empathetic",
646
+ "excited",
647
+ "friendly",
648
+ "hopeful",
649
+ "sad",
650
+ "terrified",
651
+ "whispering",
652
+ "poetry-reading",
653
+ "sports_commentary",
654
+ "sports_commentary_excited",
655
+ "story"
656
+ ],
657
+ "zh-CN-YunxiNeural": [
658
+ "narration-relaxed",
659
+ "embarrassed",
660
+ "fearful",
661
+ "sad",
662
+ "disgruntled",
663
+ "serious",
664
+ "angry",
665
+ "depressed",
666
+ "chat",
667
+ "cheerful",
668
+ "assistant"
669
+ ],
670
+ "zh-TW-HsiaoChenNeural": []
671
+ };
672
+
673
+ // packages/ssml-editor-react/src/constants/ssmlPresets.ts
561
674
  var SSML_PRESETS = [
562
675
  {
563
676
  id: "basic",
@@ -657,7 +770,7 @@ function getExpressAsStyleCategory(style) {
657
770
  }
658
771
  return "other";
659
772
  }
660
- var VOICE_STYLE_MAP = {
773
+ var LEGACY_VOICE_STYLE_MAP = {
661
774
  "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
662
775
  "ja-JP-KeitaNeural": ["chat"],
663
776
  "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
@@ -743,6 +856,7 @@ var VOICE_STYLE_MAP = {
743
856
  "de-DE-ConradNeural": ["cheerful", "sad"],
744
857
  "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
745
858
  };
859
+ var VOICE_STYLE_MAP = Object.keys(AZURE_VOICE_STYLE_MAP).length > 0 ? AZURE_VOICE_STYLE_MAP : LEGACY_VOICE_STYLE_MAP;
746
860
  var VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map(
747
861
  Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
748
862
  );
@@ -785,6 +899,7 @@ var SAY_AS_PRESETS = [
785
899
  ];
786
900
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
787
901
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
902
+ var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
788
903
  var SILENCE_TYPE_PRESETS = [
789
904
  "Leading",
790
905
  "Tailing",
@@ -793,6 +908,11 @@ var SILENCE_TYPE_PRESETS = [
793
908
  "Semicolon",
794
909
  "Enumerationcomma"
795
910
  ];
911
+ var AUDIO_DURATION_DESCRIPTIONS = {
912
+ "5s": { ja: "5\u79D2", en: "5 seconds" },
913
+ "10s": { ja: "10\u79D2", en: "10 seconds" },
914
+ "30s": { ja: "30\u79D2", en: "30 seconds" }
915
+ };
796
916
  var PHONEME_ALPHABET_PRESETS = ["ipa", "sapi", "ups", "x-sampa"];
797
917
  var VISEME_TYPE_PRESETS = ["redlips_front", "FacialExpression"];
798
918
  var SSML_ATTRIBUTE_PRESETS = {
@@ -836,6 +956,14 @@ var SSML_ATTRIBUTE_PRESETS = {
836
956
  type: SILENCE_TYPE_PRESETS,
837
957
  value: SILENCE_VALUE_PRESETS
838
958
  },
959
+ "mstts:audioduration": {
960
+ value: AUDIO_DURATION_PRESETS
961
+ },
962
+ "mstts:backgroundaudio": {
963
+ volume: PROSODY_VOLUME_PRESETS,
964
+ fadein: AUDIO_DURATION_PRESETS,
965
+ fadeout: AUDIO_DURATION_PRESETS
966
+ },
839
967
  silence: {
840
968
  type: SILENCE_TYPE_PRESETS,
841
969
  value: SILENCE_VALUE_PRESETS
@@ -1219,7 +1347,14 @@ var SSML_TAGS = {
1219
1347
  MSTTS_SILENCE: "mstts:silence",
1220
1348
  SILENCE: "silence",
1221
1349
  MSTTS_VISEME: "mstts:viseme",
1222
- VISEME: "viseme"
1350
+ VISEME: "viseme",
1351
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
1352
+ MSTTS_DIALOG: "mstts:dialog",
1353
+ MSTTS_TURN: "mstts:turn",
1354
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
1355
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
1356
+ MSTTS_EMBEDDING: "mstts:embedding",
1357
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
1223
1358
  };
1224
1359
  var SSML_ATTRS = {
1225
1360
  VERSION: "version",
@@ -1228,6 +1363,7 @@ var SSML_ATTRS = {
1228
1363
  LANG: "lang",
1229
1364
  MSTTS_XMLNS: "xmlns:mstts",
1230
1365
  NAME: "name",
1366
+ VOICE: "voice",
1231
1367
  EFFECT: "effect",
1232
1368
  RATE: "rate",
1233
1369
  PITCH: "pitch",
@@ -1259,7 +1395,9 @@ var SSML_ATTRS = {
1259
1395
  MARK: "mark",
1260
1396
  URI: "uri",
1261
1397
  TYPE: "type",
1262
- VALUE: "value"
1398
+ VALUE: "value",
1399
+ FADE_IN: "fadein",
1400
+ FADE_OUT: "fadeout"
1263
1401
  };
1264
1402
  function escapeText(value) {
1265
1403
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1346,6 +1484,23 @@ function getAttributes(element) {
1346
1484
  case SSML_TAGS.VISEME:
1347
1485
  addAttribute(attributes, SSML_ATTRS.TYPE, element.typeValue ?? element.visemeType);
1348
1486
  break;
1487
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
1488
+ addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
1489
+ break;
1490
+ case SSML_TAGS.MSTTS_TURN:
1491
+ addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1492
+ break;
1493
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1494
+ addAttribute(attributes, SSML_ATTRS.SRC, element.src);
1495
+ addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
1496
+ addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
1497
+ addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1498
+ break;
1499
+ case SSML_TAGS.MSTTS_DIALOG:
1500
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1501
+ case SSML_TAGS.MSTTS_EMBEDDING:
1502
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1503
+ break;
1349
1504
  case SSML_TAGS.PARAGRAPH:
1350
1505
  case SSML_TAGS.SENTENCE:
1351
1506
  case SSML_TAGS.WORD:
@@ -1370,6 +1525,8 @@ function getTagName(element) {
1370
1525
  case SSML_TAGS.VISEME:
1371
1526
  case SSML_TAGS.MSTTS_VISEME:
1372
1527
  return SSML_TAGS.MSTTS_VISEME;
1528
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
1529
+ return SSML_TAGS.MSTTS_AUDIO_DURATION;
1373
1530
  case "element":
1374
1531
  case "custom":
1375
1532
  return element.name;
@@ -1902,6 +2059,46 @@ function convertElement(node) {
1902
2059
  if (typeValue !== void 0) element.typeValue = typeValue;
1903
2060
  return finishElement(element, node, attributes);
1904
2061
  }
2062
+ case SSML_TAGS.MSTTS_AUDIO_DURATION: {
2063
+ const element = { type: SSML_TAGS.MSTTS_AUDIO_DURATION };
2064
+ const value = readAttribute(attributes, SSML_ATTRS.VALUE);
2065
+ if (value !== void 0) element.value = value;
2066
+ return finishElement(element, node, attributes);
2067
+ }
2068
+ case SSML_TAGS.MSTTS_DIALOG: {
2069
+ const element = { type: SSML_TAGS.MSTTS_DIALOG };
2070
+ return finishElement(element, node, attributes);
2071
+ }
2072
+ case SSML_TAGS.MSTTS_TURN: {
2073
+ const element = { type: SSML_TAGS.MSTTS_TURN };
2074
+ const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2075
+ if (voice !== void 0) element.voice = voice;
2076
+ return finishElement(element, node, attributes);
2077
+ }
2078
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
2079
+ const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
2080
+ const src = readAttribute(attributes, SSML_ATTRS.SRC);
2081
+ const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
2082
+ const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
2083
+ const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
2084
+ if (src !== void 0) element.src = src;
2085
+ if (volume !== void 0) element.volume = volume;
2086
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
2087
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
2088
+ return finishElement(element, node, attributes);
2089
+ }
2090
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2091
+ const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2092
+ return finishElement(element, node, attributes);
2093
+ }
2094
+ case SSML_TAGS.MSTTS_EMBEDDING: {
2095
+ const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2096
+ return finishElement(element, node, attributes);
2097
+ }
2098
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2099
+ const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2100
+ return finishElement(element, node, attributes);
2101
+ }
1905
2102
  default: {
1906
2103
  const element = {
1907
2104
  name: node.name,
@@ -2022,6 +2219,644 @@ function validateSsml(xmlString) {
2022
2219
  };
2023
2220
  }
2024
2221
  }
2222
+ var AZURE_VOICE_DEFINITIONS = [
2223
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2224
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2225
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
2226
+ {
2227
+ name: "en-US-GuyNeural",
2228
+ locale: "en-US",
2229
+ styles: [
2230
+ "angry",
2231
+ "cheerful",
2232
+ "excited",
2233
+ "friendly",
2234
+ "hopeful",
2235
+ "newscast",
2236
+ "sad",
2237
+ "shouting",
2238
+ "terrified",
2239
+ "unfriendly",
2240
+ "whispering"
2241
+ ]
2242
+ },
2243
+ {
2244
+ name: "en-US-JennyMultilingualNeural",
2245
+ locale: "en-US",
2246
+ styles: [
2247
+ "cheerful",
2248
+ "empathetic",
2249
+ "excited",
2250
+ "friendly",
2251
+ "hopeful",
2252
+ "sad",
2253
+ "shouting",
2254
+ "terrified",
2255
+ "unfriendly",
2256
+ "whispering"
2257
+ ]
2258
+ },
2259
+ {
2260
+ name: "en-US-JennyNeural",
2261
+ locale: "en-US",
2262
+ styles: [
2263
+ "assistant",
2264
+ "chat",
2265
+ "customerservice",
2266
+ "newscast",
2267
+ "cheerful",
2268
+ "empathetic",
2269
+ "excited",
2270
+ "friendly",
2271
+ "hopeful",
2272
+ "sad",
2273
+ "shouting",
2274
+ "terrified",
2275
+ "unfriendly",
2276
+ "whispering"
2277
+ ]
2278
+ },
2279
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
2280
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
2281
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2282
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2283
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
2284
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
2285
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
2286
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
2287
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
2288
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
2289
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
2290
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
2291
+ {
2292
+ name: "ru-RU-SvetlanaNeural",
2293
+ locale: "ru-RU",
2294
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
2295
+ },
2296
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
2297
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
2298
+ {
2299
+ name: "zh-CN-XiaoxiaoNeural",
2300
+ locale: "zh-CN",
2301
+ styles: [
2302
+ "assistant",
2303
+ "chat",
2304
+ "customerservice",
2305
+ "newscast",
2306
+ "cheerful",
2307
+ "empathetic",
2308
+ "excited",
2309
+ "friendly",
2310
+ "hopeful",
2311
+ "sad",
2312
+ "terrified",
2313
+ "whispering",
2314
+ "poetry-reading",
2315
+ "sports_commentary",
2316
+ "sports_commentary_excited",
2317
+ "story"
2318
+ ]
2319
+ },
2320
+ {
2321
+ name: "zh-CN-YunxiNeural",
2322
+ locale: "zh-CN",
2323
+ styles: [
2324
+ "narration-relaxed",
2325
+ "embarrassed",
2326
+ "fearful",
2327
+ "sad",
2328
+ "disgruntled",
2329
+ "serious",
2330
+ "angry",
2331
+ "depressed",
2332
+ "chat",
2333
+ "cheerful",
2334
+ "assistant"
2335
+ ]
2336
+ },
2337
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
2338
+ ];
2339
+ var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
2340
+ var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
2341
+ "characters",
2342
+ "spell-out",
2343
+ "cardinal",
2344
+ "ordinal",
2345
+ "number",
2346
+ "date",
2347
+ "time",
2348
+ "telephone",
2349
+ "fraction",
2350
+ "address",
2351
+ "name",
2352
+ "currency",
2353
+ "number_digit"
2354
+ ]);
2355
+ var ALLOWED_ROLES = /* @__PURE__ */ new Set([
2356
+ "Girl",
2357
+ "Boy",
2358
+ "YoungAdultFemale",
2359
+ "YoungAdultMale",
2360
+ "OlderAdultFemale",
2361
+ "OlderAdultMale",
2362
+ "SeniorFemale",
2363
+ "SeniorMale"
2364
+ ]);
2365
+ var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
2366
+ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
2367
+ "Leading",
2368
+ "Tailing",
2369
+ "Sentenceboundary",
2370
+ "Comma",
2371
+ "Semicolon",
2372
+ "Enumerationcomma"
2373
+ ]);
2374
+ var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
2375
+ function decodeAttribute(value) {
2376
+ return value.replace(
2377
+ /&(?:amp|apos|gt|lt|quot);/gi,
2378
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
2379
+ );
2380
+ }
2381
+ function findTagEnd2(source, start) {
2382
+ let quote = "";
2383
+ for (let index = start; index < source.length; index += 1) {
2384
+ const character = source[index];
2385
+ if (quote) {
2386
+ if (character === quote) quote = "";
2387
+ } else if (character === '"' || character === "'") quote = character;
2388
+ else if (character === ">") return index;
2389
+ }
2390
+ return source.length - 1;
2391
+ }
2392
+ function tokenizeElements(source) {
2393
+ const tokens = [];
2394
+ const openElements = [];
2395
+ let index = 0;
2396
+ while (index < source.length) {
2397
+ const start = source.indexOf("<", index);
2398
+ if (start === -1) break;
2399
+ if (source.startsWith("<!--", start)) {
2400
+ const end2 = source.indexOf("-->", start + 4);
2401
+ index = end2 === -1 ? source.length : end2 + 3;
2402
+ continue;
2403
+ }
2404
+ if (source.startsWith("<![CDATA[", start)) {
2405
+ const end2 = source.indexOf("]]>", start + 9);
2406
+ index = end2 === -1 ? source.length : end2 + 3;
2407
+ continue;
2408
+ }
2409
+ if (source.startsWith("<?", start)) {
2410
+ const end2 = source.indexOf("?>", start + 2);
2411
+ index = end2 === -1 ? source.length : end2 + 2;
2412
+ continue;
2413
+ }
2414
+ const end = findTagEnd2(source, start + 1);
2415
+ const raw = source.slice(start, end + 1);
2416
+ if (raw.startsWith("</")) {
2417
+ openElements.pop();
2418
+ index = end + 1;
2419
+ continue;
2420
+ }
2421
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
2422
+ if (!nameMatch?.[1]) {
2423
+ index = end + 1;
2424
+ continue;
2425
+ }
2426
+ const attributes = /* @__PURE__ */ new Map();
2427
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
2428
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
2429
+ for (const match of attributeSource.matchAll(attributePattern)) {
2430
+ attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
2431
+ }
2432
+ const selfClosing = /\/\s*>$/.test(raw);
2433
+ const parent = openElements[openElements.length - 1];
2434
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2435
+ const tokenName = nameMatch[1];
2436
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2437
+ tokens.push({
2438
+ attributes,
2439
+ end,
2440
+ name: tokenName,
2441
+ parentName: parent?.name,
2442
+ parentVoiceName,
2443
+ selfClosing,
2444
+ start
2445
+ });
2446
+ if (!selfClosing) {
2447
+ openElements.push({
2448
+ name: tokenName,
2449
+ voiceName: tokenVoiceName
2450
+ });
2451
+ }
2452
+ index = end + 1;
2453
+ }
2454
+ return tokens;
2455
+ }
2456
+ function location(source, offset) {
2457
+ const before = source.slice(0, Math.max(0, offset));
2458
+ const line = before.split("\n").length;
2459
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2460
+ }
2461
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
2462
+ diagnostics.push({
2463
+ ...location(source, offset),
2464
+ message,
2465
+ severity,
2466
+ source: "ssml-static-validator",
2467
+ ...code2 ? { code: code2 } : {}
2468
+ });
2469
+ }
2470
+ function isSupportedProsodyRate(value) {
2471
+ const trimmed = value.trim();
2472
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
2473
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
2474
+ if (!multiplier) return false;
2475
+ const numericValue = Number(multiplier[1]);
2476
+ return numericValue >= 0.5 && numericValue <= 2;
2477
+ }
2478
+ function isValidAzureAudioDuration(value) {
2479
+ const trimmed = value.trim();
2480
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
2481
+ if (numeric) return Number(numeric[1]) > 0;
2482
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
2483
+ if (!clock) return false;
2484
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2485
+ }
2486
+ function attr(token, name) {
2487
+ return token.attributes.get(name.toLowerCase());
2488
+ }
2489
+ var DEFAULT_LANGUAGE_ALIASES = {
2490
+ "zh-CN": ["zh-Hans"],
2491
+ "zh-TW": ["zh-Hant"]
2492
+ };
2493
+ function canonicalLanguageTag(language) {
2494
+ const trimmed = language.trim();
2495
+ if (!trimmed) return "";
2496
+ try {
2497
+ return new Intl.Locale(trimmed).toString().toLowerCase();
2498
+ } catch {
2499
+ return trimmed.toLowerCase();
2500
+ }
2501
+ }
2502
+ function createLanguageNormalizer(options) {
2503
+ const aliases = /* @__PURE__ */ new Map();
2504
+ const addAliasGroup = (canonical, values) => {
2505
+ const normalizedCanonical = canonicalLanguageTag(canonical);
2506
+ if (!normalizedCanonical) return;
2507
+ aliases.set(normalizedCanonical, normalizedCanonical);
2508
+ for (const value of values) {
2509
+ const normalizedValue = canonicalLanguageTag(value);
2510
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
2511
+ }
2512
+ };
2513
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
2514
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
2515
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
2516
+ return (language) => {
2517
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
2518
+ const normalized = canonicalLanguageTag(customValue);
2519
+ return aliases.get(normalized) ?? normalized;
2520
+ };
2521
+ }
2522
+ function voiceLocalePrefix(voiceName) {
2523
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
2524
+ if (!match?.groups) return void 0;
2525
+ const tag = `${match.groups.language}-${match.groups.region}`;
2526
+ return {
2527
+ language: match.groups.language.toLowerCase(),
2528
+ region: match.groups.region.toLowerCase(),
2529
+ tag
2530
+ };
2531
+ }
2532
+ function definitionFromStyleMap(voiceName, styles) {
2533
+ return {
2534
+ name: voiceName,
2535
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
2536
+ styles
2537
+ };
2538
+ }
2539
+ function normalizeVoiceCatalog(options) {
2540
+ const definitions = /* @__PURE__ */ new Map();
2541
+ for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
2542
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
2543
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
2544
+ for (const definition of options.customVoiceDefinitions ?? [])
2545
+ definitions.set(definition.name.toLowerCase(), definition);
2546
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
2547
+ const key = voiceName.toLowerCase();
2548
+ const current = definitions.get(key);
2549
+ definitions.set(key, {
2550
+ ...current ?? definitionFromStyleMap(voiceName, styles),
2551
+ name: current?.name ?? voiceName,
2552
+ styles: styles.map((style) => style.toLowerCase())
2553
+ });
2554
+ }
2555
+ return definitions;
2556
+ }
2557
+ function diagnosticSeverity(policy) {
2558
+ if (policy === "ignore") return void 0;
2559
+ return policy === "error" ? "error" : "warning";
2560
+ }
2561
+ function languagePart(language) {
2562
+ try {
2563
+ return new Intl.Locale(language).language.toLowerCase();
2564
+ } catch {
2565
+ return language.split("-")[0]?.toLowerCase() ?? "";
2566
+ }
2567
+ }
2568
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
2569
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
2570
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
2571
+ const normalizedLanguage = normalizeLanguage(language);
2572
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
2573
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
2574
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
2575
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
2576
+ }
2577
+ function canonicalTagName(name) {
2578
+ const normalized = name.toLowerCase();
2579
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
2580
+ if (normalized === "sayas") return "say-as";
2581
+ return normalized;
2582
+ }
2583
+ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
2584
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
2585
+ return;
2586
+ const tagName = canonicalTagName(token.name);
2587
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
2588
+ const supportedTags = definition.supportedTags?.map(canonicalTagName);
2589
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
2590
+ addDiagnostic(
2591
+ diagnostics,
2592
+ source,
2593
+ token.start,
2594
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
2595
+ "error",
2596
+ "azure-unsupported-tag-for-voice"
2597
+ );
2598
+ }
2599
+ }
2600
+ function validateAudioSource(token, source, diagnostics, options, elementName2) {
2601
+ const src = attr(token, "src");
2602
+ if (!src) {
2603
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2604
+ return;
2605
+ }
2606
+ let parsed;
2607
+ try {
2608
+ parsed = new URL(src);
2609
+ } catch {
2610
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2611
+ return;
2612
+ }
2613
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2614
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2615
+ if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
2616
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2617
+ else if (!options.allowExternalAudio)
2618
+ addDiagnostic(
2619
+ diagnostics,
2620
+ source,
2621
+ token.start,
2622
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2623
+ );
2624
+ }
2625
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
2626
+ const name = token.name.toLowerCase();
2627
+ if (name === "voice" && !attr(token, "name")?.trim())
2628
+ addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
2629
+ if (name === "break") {
2630
+ const time = attr(token, "time");
2631
+ const strength = attr(token, "strength");
2632
+ if (!time && !strength)
2633
+ addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
2634
+ if (time && strength)
2635
+ addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
2636
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
2637
+ addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
2638
+ if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
2639
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
2640
+ }
2641
+ if (name === "prosody") {
2642
+ const rate = attr(token, "rate");
2643
+ const pitch = attr(token, "pitch");
2644
+ const volume = attr(token, "volume");
2645
+ if (rate && !isSupportedProsodyRate(rate))
2646
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
2647
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
2648
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
2649
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
2650
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
2651
+ }
2652
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
2653
+ const style = attr(token, "style");
2654
+ if (!style?.trim())
2655
+ addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
2656
+ const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
2657
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
2658
+ addDiagnostic(
2659
+ diagnostics,
2660
+ source,
2661
+ token.start,
2662
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
2663
+ );
2664
+ const role = attr(token, "role");
2665
+ if (role && !ALLOWED_ROLES.has(role))
2666
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
2667
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
2668
+ const supportedStyles = definition?.styles;
2669
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
2670
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
2671
+ addDiagnostic(
2672
+ diagnostics,
2673
+ source,
2674
+ token.start,
2675
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
2676
+ severity,
2677
+ "azure-unsupported-style"
2678
+ );
2679
+ if (style && voiceName && !definition && severity)
2680
+ addDiagnostic(
2681
+ diagnostics,
2682
+ source,
2683
+ token.start,
2684
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
2685
+ severity
2686
+ );
2687
+ }
2688
+ if (name === "say-as" || name === "sayas") {
2689
+ const interpretAs = attr(token, "interpret-as");
2690
+ if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
2691
+ addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
2692
+ }
2693
+ if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
2694
+ addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
2695
+ if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
2696
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
2697
+ if (name === "sub" && !attr(token, "alias")?.trim())
2698
+ addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
2699
+ if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
2700
+ addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
2701
+ if (name === "mark" && !attr(token, "name")?.trim())
2702
+ addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
2703
+ if (name === "bookmark" && !attr(token, "mark")?.trim())
2704
+ addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
2705
+ if (name === "lexicon") {
2706
+ const uri = attr(token, "uri");
2707
+ if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
2708
+ else {
2709
+ try {
2710
+ const parsed = new URL(uri);
2711
+ if (parsed.protocol !== "https:")
2712
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
2713
+ } catch {
2714
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
2715
+ }
2716
+ }
2717
+ }
2718
+ if (name === "mstts:silence") {
2719
+ const type = attr(token, "type");
2720
+ const value = attr(token, "value");
2721
+ if (!type || !ALLOWED_SILENCE_TYPES.has(type))
2722
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
2723
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
2724
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
2725
+ }
2726
+ if (name === "mstts:audioduration") {
2727
+ const value = attr(token, "value");
2728
+ if (!value || !isValidAzureAudioDuration(value))
2729
+ addDiagnostic(
2730
+ diagnostics,
2731
+ source,
2732
+ token.start,
2733
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
2734
+ );
2735
+ if (!token.selfClosing)
2736
+ addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
2737
+ }
2738
+ if (name === "mstts:viseme") {
2739
+ const type = attr(token, "type");
2740
+ if (!type || !ALLOWED_VISEME_TYPES.has(type))
2741
+ addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
2742
+ }
2743
+ if (name === "audio") {
2744
+ validateAudioSource(token, source, diagnostics, options, "audio");
2745
+ }
2746
+ if (name === "mstts:turn") {
2747
+ if (!attr(token, "voice")?.trim())
2748
+ addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
2749
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
2750
+ addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
2751
+ }
2752
+ if (name === "mstts:backgroundaudio") {
2753
+ validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
2754
+ const volume = attr(token, "volume");
2755
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
2756
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
2757
+ for (const [attribute, value] of [
2758
+ ["fadein", attr(token, "fadein")],
2759
+ ["fadeout", attr(token, "fadeout")]
2760
+ ]) {
2761
+ if (value && !isValidAzureAudioDuration(value))
2762
+ addDiagnostic(
2763
+ diagnostics,
2764
+ source,
2765
+ token.start,
2766
+ `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2767
+ );
2768
+ }
2769
+ if (!token.selfClosing)
2770
+ addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
2771
+ }
2772
+ }
2773
+ function validateAzureSsml(ssml, options = {}) {
2774
+ const diagnostics = [];
2775
+ if (typeof ssml !== "string") {
2776
+ return [
2777
+ {
2778
+ line: 1,
2779
+ column: 1,
2780
+ message: "SSML input must be a string",
2781
+ severity: "error",
2782
+ source: "ssml-static-validator"
2783
+ }
2784
+ ];
2785
+ }
2786
+ const maxLength = options.maxLength ?? 1e4;
2787
+ if (ssml.length > maxLength)
2788
+ addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
2789
+ try {
2790
+ parseSsml(ssml);
2791
+ } catch (error) {
2792
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
2793
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
2794
+ addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
2795
+ return diagnostics;
2796
+ }
2797
+ const tokens = tokenizeElements(ssml);
2798
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2799
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2800
+ if (!speak || voices.length === 0)
2801
+ addDiagnostic(
2802
+ diagnostics,
2803
+ ssml,
2804
+ speak?.start ?? 0,
2805
+ "Azure SSML requires at least one <voice> element under <speak>."
2806
+ );
2807
+ const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
2808
+ const voiceCatalog = normalizeVoiceCatalog(options);
2809
+ const normalizeLanguage = createLanguageNormalizer(options);
2810
+ const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
2811
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
2812
+ for (const token of voicesToValidate) {
2813
+ const name = attr(token, "name")?.trim();
2814
+ const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
2815
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2816
+ if (name && !definition && policySeverity)
2817
+ addDiagnostic(
2818
+ diagnostics,
2819
+ ssml,
2820
+ token.start,
2821
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
2822
+ policySeverity,
2823
+ "azure-unknown-voice"
2824
+ );
2825
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
2826
+ addDiagnostic(
2827
+ diagnostics,
2828
+ ssml,
2829
+ token.start,
2830
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
2831
+ "warning",
2832
+ "azure-locale-mismatch"
2833
+ );
2834
+ }
2835
+ for (const token of tokens) {
2836
+ const tokenName = token.name.toLowerCase();
2837
+ const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2838
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2839
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2840
+ validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2841
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2842
+ addDiagnostic(
2843
+ diagnostics,
2844
+ ssml,
2845
+ token.start,
2846
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
2847
+ "error",
2848
+ "azure-unsupported-model-for-voice"
2849
+ );
2850
+ }
2851
+ }
2852
+ return diagnostics;
2853
+ }
2854
+ var AZURE_VOICE_CATALOG_METADATA = {
2855
+ apiVersion: "2025-10-01",
2856
+ generatedAt: "2026-08-28T00:00:00.000Z",
2857
+ regions: [],
2858
+ voiceCount: AZURE_VOICE_DEFINITIONS.length
2859
+ };
2025
2860
 
2026
2861
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
2027
2862
  function getDocumentChildren(document) {
@@ -2082,6 +2917,7 @@ var INTRINSICALLY_EMPTY_ELEMENTS = /* @__PURE__ */ new Set([
2082
2917
  "lexicon",
2083
2918
  "mark",
2084
2919
  "mstts:silence",
2920
+ "mstts:audioduration",
2085
2921
  "mstts:viseme",
2086
2922
  "silence",
2087
2923
  "viseme"
@@ -2145,7 +2981,7 @@ function validateEntityReferences(value) {
2145
2981
  index = semicolon + 1;
2146
2982
  }
2147
2983
  }
2148
- function findTagEnd2(source, start, hasInternalSubset = false) {
2984
+ function findTagEnd3(source, start, hasInternalSubset = false) {
2149
2985
  let quote;
2150
2986
  let subsetDepth = 0;
2151
2987
  for (let index = start + 1; index < source.length; index += 1) {
@@ -2318,7 +3154,7 @@ function parseXml(source) {
2318
3154
  continue;
2319
3155
  }
2320
3156
  if (source.startsWith("</", index)) {
2321
- const end2 = findTagEnd2(source, index);
3157
+ const end2 = findTagEnd3(source, index);
2322
3158
  const name2 = parseClosingTag(source, index, end2);
2323
3159
  const element = currentElement(stack);
2324
3160
  if (element === void 0 || element.name !== name2) {
@@ -2330,7 +3166,7 @@ function parseXml(source) {
2330
3166
  continue;
2331
3167
  }
2332
3168
  if (source.startsWith("<!", index)) {
2333
- const end2 = findTagEnd2(source, index, true);
3169
+ const end2 = findTagEnd3(source, index, true);
2334
3170
  parseDeclaration(source, index, end2);
2335
3171
  appendNode2(currentElement(stack)?.children ?? children, {
2336
3172
  kind: "declaration",
@@ -2339,7 +3175,7 @@ function parseXml(source) {
2339
3175
  index = end2 + 1;
2340
3176
  continue;
2341
3177
  }
2342
- const end = findTagEnd2(source, index);
3178
+ const end = findTagEnd3(source, index);
2343
3179
  const { name, selfClosing } = parseStartTag(source, index, end);
2344
3180
  if (stack.length === 0) {
2345
3181
  if (root !== void 0) {
@@ -2802,6 +3638,28 @@ var SSML_COMPLETION_SNIPPETS = [
2802
3638
  label: "mstts:express-as",
2803
3639
  insertText: `<mstts:express-as style="cheerful">\${1:text}</mstts:express-as>`
2804
3640
  },
3641
+ {
3642
+ label: "mstts:audioduration",
3643
+ insertText: '<mstts:audioduration value="10s" />'
3644
+ },
3645
+ {
3646
+ label: "mstts:dialog",
3647
+ insertText: `<mstts:dialog>
3648
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
3649
+ </mstts:dialog>`
3650
+ },
3651
+ {
3652
+ label: "mstts:turn",
3653
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
3654
+ },
3655
+ {
3656
+ label: "mstts:backgroundaudio",
3657
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
3658
+ },
3659
+ {
3660
+ label: "mstts:ttsembedding",
3661
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
3662
+ },
2805
3663
  {
2806
3664
  label: "sub",
2807
3665
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -3161,6 +4019,74 @@ var SSML_TAG_DEFINITIONS = [
3161
4019
  values: VISEME_TYPE_PRESETS2
3162
4020
  }
3163
4021
  ]
4022
+ },
4023
+ {
4024
+ name: "mstts:audioduration",
4025
+ description: "Sets the target duration of synthesized audio.",
4026
+ parameters: [
4027
+ {
4028
+ name: "value",
4029
+ description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`.",
4030
+ example: "10s"
4031
+ }
4032
+ ]
4033
+ },
4034
+ {
4035
+ name: "mstts:dialog",
4036
+ description: "Groups multiple Azure dialog turns that can use different voices.",
4037
+ parameters: []
4038
+ },
4039
+ {
4040
+ name: "mstts:turn",
4041
+ description: "Adds one dialog turn using the required Azure voice name.",
4042
+ parameters: [
4043
+ {
4044
+ name: "voice",
4045
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
4046
+ example: "en-US-JennyNeural"
4047
+ }
4048
+ ]
4049
+ },
4050
+ {
4051
+ name: "mstts:backgroundaudio",
4052
+ description: "Plays background audio while speech is synthesized.",
4053
+ parameters: [
4054
+ {
4055
+ name: "src",
4056
+ description: "An absolute HTTP(S) URL for the background audio file.",
4057
+ example: "https://example.com/music.mp3"
4058
+ },
4059
+ {
4060
+ name: "volume",
4061
+ description: "The background audio volume, for example `-3dB` or `medium`.",
4062
+ example: "-3dB"
4063
+ },
4064
+ {
4065
+ name: "fadein",
4066
+ description: "The fade-in duration, for example `1s`.",
4067
+ example: "1s"
4068
+ },
4069
+ {
4070
+ name: "fadeout",
4071
+ description: "The fade-out duration, for example `500ms`.",
4072
+ example: "500ms"
4073
+ }
4074
+ ]
4075
+ },
4076
+ {
4077
+ name: "mstts:ttsembedding",
4078
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
4079
+ parameters: []
4080
+ },
4081
+ {
4082
+ name: "mstts:embedding",
4083
+ description: "Specifies embedding metadata for custom voice scenarios.",
4084
+ parameters: []
4085
+ },
4086
+ {
4087
+ name: "mstts:voiceconversion",
4088
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
4089
+ parameters: []
3164
4090
  }
3165
4091
  ];
3166
4092
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -3219,7 +4145,7 @@ function toRange(source, token) {
3219
4145
  function containsOffset(token, offset) {
3220
4146
  return offset >= token.start && offset < token.end;
3221
4147
  }
3222
- function findTagEnd3(source, start) {
4148
+ function findTagEnd4(source, start) {
3223
4149
  let quote;
3224
4150
  for (let index = start; index < source.length; index += 1) {
3225
4151
  const character = source[index];
@@ -3350,7 +4276,7 @@ function findTagAtOffset(source, offset) {
3350
4276
  searchStart = tokenEnd2;
3351
4277
  continue;
3352
4278
  }
3353
- const tagEnd = findTagEnd3(source, start + 1);
4279
+ const tagEnd = findTagEnd4(source, start + 1);
3354
4280
  const contentEnd = tagEnd ?? source.length;
3355
4281
  const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
3356
4282
  if (offset < tokenEnd) {
@@ -3477,6 +4403,8 @@ export {
3477
4403
  SAY_AS_PRESETS,
3478
4404
  LANGUAGE_PRESETS,
3479
4405
  SILENCE_VALUE_PRESETS,
4406
+ AUDIO_DURATION_PRESETS,
4407
+ AUDIO_DURATION_DESCRIPTIONS,
3480
4408
  SSML_ATTRIBUTE_PRESETS,
3481
4409
  BREAK_TIME_DESCRIPTIONS,
3482
4410
  EMPHASIS_LEVEL_DESCRIPTIONS,
@@ -3497,6 +4425,7 @@ export {
3497
4425
  parseSsml,
3498
4426
  buildPartialSsml,
3499
4427
  validateSsml,
4428
+ validateAzureSsml,
3500
4429
  clearSsmlDocument,
3501
4430
  formatXmlFragment,
3502
4431
  getEditableRegion,
@@ -3510,4 +4439,4 @@ export {
3510
4439
  findSsmlHoverTarget,
3511
4440
  formatSsmlHover
3512
4441
  };
3513
- //# sourceMappingURL=chunk-4RQETJUI.mjs.map
4442
+ //# sourceMappingURL=chunk-JNJVTEL6.mjs.map