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.
package/dist/react.js CHANGED
@@ -36,6 +36,7 @@ __export(react_exports, {
36
36
  SSML_HOVER_COPY: () => SSML_HOVER_COPY,
37
37
  SSML_INSERTIONS: () => SSML_INSERTIONS,
38
38
  SsmlEditor: () => SsmlEditor,
39
+ VisualSsmlEditor: () => VisualSsmlEditor,
39
40
  createSsmlEditorInsertionDefinition: () => createSsmlEditorInsertionDefinition,
40
41
  registerSsmlCodeLens: () => registerSsmlCodeLens,
41
42
  updateTagAttribute: () => updateTagAttribute
@@ -43,8 +44,8 @@ __export(react_exports, {
43
44
  module.exports = __toCommonJS(react_exports);
44
45
 
45
46
  // packages/ssml-editor-react/src/SsmlEditor.tsx
46
- var import_react3 = require("react");
47
- var import_react4 = __toESM(require("@monaco-editor/react"));
47
+ var import_react4 = require("react");
48
+ var import_react5 = __toESM(require("@monaco-editor/react"));
48
49
 
49
50
  // packages/ssml-editor-react/src/buttonVisibility.ts
50
51
  function isSsmlEditorButtonVisible(buttonVisibility, button) {
@@ -54,6 +55,8 @@ function isSsmlEditorButtonVisible(buttonVisibility, button) {
54
55
  // packages/ssml-editor-react/src/constants/ssmlPresets.ts
55
56
  var ssmlPresets_exports = {};
56
57
  __export(ssmlPresets_exports, {
58
+ AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
59
+ AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
57
60
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
58
61
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
59
62
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -86,6 +89,103 @@ __export(ssmlPresets_exports, {
86
89
  getExpressAsStyleCategory: () => getExpressAsStyleCategory,
87
90
  resolveExpressAsStyles: () => resolveExpressAsStyles
88
91
  });
92
+
93
+ // packages/ssml-editor-react/src/constants/azureVoiceStyleMap.generated.ts
94
+ var AZURE_VOICE_STYLE_MAP = {
95
+ "de-DE-ConradNeural": ["cheerful", "sad"],
96
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
97
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
98
+ "en-US-GuyNeural": [
99
+ "angry",
100
+ "cheerful",
101
+ "excited",
102
+ "friendly",
103
+ "hopeful",
104
+ "newscast",
105
+ "sad",
106
+ "shouting",
107
+ "terrified",
108
+ "unfriendly",
109
+ "whispering"
110
+ ],
111
+ "en-US-JennyMultilingualNeural": [
112
+ "cheerful",
113
+ "empathetic",
114
+ "excited",
115
+ "friendly",
116
+ "hopeful",
117
+ "sad",
118
+ "shouting",
119
+ "terrified",
120
+ "unfriendly",
121
+ "whispering"
122
+ ],
123
+ "en-US-JennyNeural": [
124
+ "assistant",
125
+ "chat",
126
+ "customerservice",
127
+ "newscast",
128
+ "cheerful",
129
+ "empathetic",
130
+ "excited",
131
+ "friendly",
132
+ "hopeful",
133
+ "sad",
134
+ "shouting",
135
+ "terrified",
136
+ "unfriendly",
137
+ "whispering"
138
+ ],
139
+ "es-ES-ElviraNeural": [],
140
+ "fil-PH-AngeloNeural": [],
141
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
142
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
143
+ "id-ID-GadisNeural": [],
144
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
145
+ "ja-JP-KeitaNeural": ["chat"],
146
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
147
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
148
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
149
+ "ms-MY-YasminNeural": [],
150
+ "pt-BR-FranciscaNeural": ["calm"],
151
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
152
+ "th-TH-PremwadeeNeural": [],
153
+ "vi-VN-HoaiMyNeural": [],
154
+ "zh-CN-XiaoxiaoNeural": [
155
+ "assistant",
156
+ "chat",
157
+ "customerservice",
158
+ "newscast",
159
+ "cheerful",
160
+ "empathetic",
161
+ "excited",
162
+ "friendly",
163
+ "hopeful",
164
+ "sad",
165
+ "terrified",
166
+ "whispering",
167
+ "poetry-reading",
168
+ "sports_commentary",
169
+ "sports_commentary_excited",
170
+ "story"
171
+ ],
172
+ "zh-CN-YunxiNeural": [
173
+ "narration-relaxed",
174
+ "embarrassed",
175
+ "fearful",
176
+ "sad",
177
+ "disgruntled",
178
+ "serious",
179
+ "angry",
180
+ "depressed",
181
+ "chat",
182
+ "cheerful",
183
+ "assistant"
184
+ ],
185
+ "zh-TW-HsiaoChenNeural": []
186
+ };
187
+
188
+ // packages/ssml-editor-react/src/constants/ssmlPresets.ts
89
189
  var SSML_PRESETS = [
90
190
  {
91
191
  id: "basic",
@@ -185,7 +285,7 @@ function getExpressAsStyleCategory(style) {
185
285
  }
186
286
  return "other";
187
287
  }
188
- var VOICE_STYLE_MAP = {
288
+ var LEGACY_VOICE_STYLE_MAP = {
189
289
  "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
190
290
  "ja-JP-KeitaNeural": ["chat"],
191
291
  "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
@@ -271,6 +371,7 @@ var VOICE_STYLE_MAP = {
271
371
  "de-DE-ConradNeural": ["cheerful", "sad"],
272
372
  "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
273
373
  };
374
+ var VOICE_STYLE_MAP = Object.keys(AZURE_VOICE_STYLE_MAP).length > 0 ? AZURE_VOICE_STYLE_MAP : LEGACY_VOICE_STYLE_MAP;
274
375
  var VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map(
275
376
  Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
276
377
  );
@@ -313,6 +414,7 @@ var SAY_AS_PRESETS = [
313
414
  ];
314
415
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
315
416
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
417
+ var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
316
418
  var SILENCE_TYPE_PRESETS = [
317
419
  "Leading",
318
420
  "Tailing",
@@ -321,6 +423,11 @@ var SILENCE_TYPE_PRESETS = [
321
423
  "Semicolon",
322
424
  "Enumerationcomma"
323
425
  ];
426
+ var AUDIO_DURATION_DESCRIPTIONS = {
427
+ "5s": { ja: "5\u79D2", en: "5 seconds" },
428
+ "10s": { ja: "10\u79D2", en: "10 seconds" },
429
+ "30s": { ja: "30\u79D2", en: "30 seconds" }
430
+ };
324
431
  var PHONEME_ALPHABET_PRESETS = ["ipa", "sapi", "ups", "x-sampa"];
325
432
  var VISEME_TYPE_PRESETS = ["redlips_front", "FacialExpression"];
326
433
  var SSML_ATTRIBUTE_PRESETS = {
@@ -364,6 +471,14 @@ var SSML_ATTRIBUTE_PRESETS = {
364
471
  type: SILENCE_TYPE_PRESETS,
365
472
  value: SILENCE_VALUE_PRESETS
366
473
  },
474
+ "mstts:audioduration": {
475
+ value: AUDIO_DURATION_PRESETS
476
+ },
477
+ "mstts:backgroundaudio": {
478
+ volume: PROSODY_VOLUME_PRESETS,
479
+ fadein: AUDIO_DURATION_PRESETS,
480
+ fadeout: AUDIO_DURATION_PRESETS
481
+ },
367
482
  silence: {
368
483
  type: SILENCE_TYPE_PRESETS,
369
484
  value: SILENCE_VALUE_PRESETS
@@ -910,6 +1025,13 @@ var SSML_HOVER_COPY = {
910
1025
  parameters: {
911
1026
  type: { title: "type", description: "\u30D3\u30BC\u30FC\u30E0\u30A4\u30D9\u30F3\u30C8\u306E\u5F62\u5F0F\u3002" }
912
1027
  }
1028
+ },
1029
+ "mstts:audioduration": {
1030
+ title: "\u97F3\u58F0\u9577",
1031
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1032
+ parameters: {
1033
+ value: { title: "value", description: "\u76EE\u6A19\u6642\u9593\u3002\u4F8B: `10s`\u3001`5000ms`\u3001`00:00:10`\u3002" }
1034
+ }
913
1035
  }
914
1036
  }
915
1037
  },
@@ -1067,6 +1189,13 @@ var SSML_HOVER_COPY = {
1067
1189
  parameters: {
1068
1190
  type: { title: "type", description: "The viseme event format." }
1069
1191
  }
1192
+ },
1193
+ "mstts:audioduration": {
1194
+ title: "Audio duration",
1195
+ description: "Sets the target duration of synthesized audio.",
1196
+ parameters: {
1197
+ value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." }
1198
+ }
1070
1199
  }
1071
1200
  }
1072
1201
  }
@@ -1166,6 +1295,14 @@ var editorStyles = {
1166
1295
  display: "grid",
1167
1296
  gap: "0.5rem"
1168
1297
  },
1298
+ visual: {
1299
+ display: "grid",
1300
+ gap: "0.75rem",
1301
+ minHeight: "8rem",
1302
+ padding: "0.75rem",
1303
+ border: "1px solid var(--ssml-editor-control-border)",
1304
+ borderRadius: "0.25rem"
1305
+ },
1169
1306
  toolbarIconOnly: {
1170
1307
  justifyContent: "center",
1171
1308
  minWidth: "2.25rem",
@@ -1570,7 +1707,14 @@ var SSML_TAGS = {
1570
1707
  MSTTS_SILENCE: "mstts:silence",
1571
1708
  SILENCE: "silence",
1572
1709
  MSTTS_VISEME: "mstts:viseme",
1573
- VISEME: "viseme"
1710
+ VISEME: "viseme",
1711
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
1712
+ MSTTS_DIALOG: "mstts:dialog",
1713
+ MSTTS_TURN: "mstts:turn",
1714
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
1715
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
1716
+ MSTTS_EMBEDDING: "mstts:embedding",
1717
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
1574
1718
  };
1575
1719
  var SSML_ATTRS = {
1576
1720
  VERSION: "version",
@@ -1579,6 +1723,7 @@ var SSML_ATTRS = {
1579
1723
  LANG: "lang",
1580
1724
  MSTTS_XMLNS: "xmlns:mstts",
1581
1725
  NAME: "name",
1726
+ VOICE: "voice",
1582
1727
  EFFECT: "effect",
1583
1728
  RATE: "rate",
1584
1729
  PITCH: "pitch",
@@ -1610,7 +1755,9 @@ var SSML_ATTRS = {
1610
1755
  MARK: "mark",
1611
1756
  URI: "uri",
1612
1757
  TYPE: "type",
1613
- VALUE: "value"
1758
+ VALUE: "value",
1759
+ FADE_IN: "fadein",
1760
+ FADE_OUT: "fadeout"
1614
1761
  };
1615
1762
  function escapeText(value) {
1616
1763
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1697,6 +1844,23 @@ function getAttributes(element) {
1697
1844
  case SSML_TAGS.VISEME:
1698
1845
  addAttribute(attributes, SSML_ATTRS.TYPE, element.typeValue ?? element.visemeType);
1699
1846
  break;
1847
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
1848
+ addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
1849
+ break;
1850
+ case SSML_TAGS.MSTTS_TURN:
1851
+ addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1852
+ break;
1853
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1854
+ addAttribute(attributes, SSML_ATTRS.SRC, element.src);
1855
+ addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
1856
+ addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
1857
+ addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1858
+ break;
1859
+ case SSML_TAGS.MSTTS_DIALOG:
1860
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1861
+ case SSML_TAGS.MSTTS_EMBEDDING:
1862
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1863
+ break;
1700
1864
  case SSML_TAGS.PARAGRAPH:
1701
1865
  case SSML_TAGS.SENTENCE:
1702
1866
  case SSML_TAGS.WORD:
@@ -1721,6 +1885,8 @@ function getTagName(element) {
1721
1885
  case SSML_TAGS.VISEME:
1722
1886
  case SSML_TAGS.MSTTS_VISEME:
1723
1887
  return SSML_TAGS.MSTTS_VISEME;
1888
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
1889
+ return SSML_TAGS.MSTTS_AUDIO_DURATION;
1724
1890
  case "element":
1725
1891
  case "custom":
1726
1892
  return element.name;
@@ -2253,6 +2419,46 @@ function convertElement(node) {
2253
2419
  if (typeValue !== void 0) element.typeValue = typeValue;
2254
2420
  return finishElement(element, node, attributes);
2255
2421
  }
2422
+ case SSML_TAGS.MSTTS_AUDIO_DURATION: {
2423
+ const element = { type: SSML_TAGS.MSTTS_AUDIO_DURATION };
2424
+ const value = readAttribute(attributes, SSML_ATTRS.VALUE);
2425
+ if (value !== void 0) element.value = value;
2426
+ return finishElement(element, node, attributes);
2427
+ }
2428
+ case SSML_TAGS.MSTTS_DIALOG: {
2429
+ const element = { type: SSML_TAGS.MSTTS_DIALOG };
2430
+ return finishElement(element, node, attributes);
2431
+ }
2432
+ case SSML_TAGS.MSTTS_TURN: {
2433
+ const element = { type: SSML_TAGS.MSTTS_TURN };
2434
+ const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2435
+ if (voice !== void 0) element.voice = voice;
2436
+ return finishElement(element, node, attributes);
2437
+ }
2438
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
2439
+ const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
2440
+ const src = readAttribute(attributes, SSML_ATTRS.SRC);
2441
+ const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
2442
+ const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
2443
+ const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
2444
+ if (src !== void 0) element.src = src;
2445
+ if (volume !== void 0) element.volume = volume;
2446
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
2447
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
2448
+ return finishElement(element, node, attributes);
2449
+ }
2450
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2451
+ const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2452
+ return finishElement(element, node, attributes);
2453
+ }
2454
+ case SSML_TAGS.MSTTS_EMBEDDING: {
2455
+ const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2456
+ return finishElement(element, node, attributes);
2457
+ }
2458
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2459
+ const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2460
+ return finishElement(element, node, attributes);
2461
+ }
2256
2462
  default: {
2257
2463
  const element = {
2258
2464
  name: node.name,
@@ -2373,6 +2579,644 @@ function validateSsml(xmlString) {
2373
2579
  };
2374
2580
  }
2375
2581
  }
2582
+ var AZURE_VOICE_DEFINITIONS = [
2583
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2584
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
2585
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
2586
+ {
2587
+ name: "en-US-GuyNeural",
2588
+ locale: "en-US",
2589
+ styles: [
2590
+ "angry",
2591
+ "cheerful",
2592
+ "excited",
2593
+ "friendly",
2594
+ "hopeful",
2595
+ "newscast",
2596
+ "sad",
2597
+ "shouting",
2598
+ "terrified",
2599
+ "unfriendly",
2600
+ "whispering"
2601
+ ]
2602
+ },
2603
+ {
2604
+ name: "en-US-JennyMultilingualNeural",
2605
+ locale: "en-US",
2606
+ styles: [
2607
+ "cheerful",
2608
+ "empathetic",
2609
+ "excited",
2610
+ "friendly",
2611
+ "hopeful",
2612
+ "sad",
2613
+ "shouting",
2614
+ "terrified",
2615
+ "unfriendly",
2616
+ "whispering"
2617
+ ]
2618
+ },
2619
+ {
2620
+ name: "en-US-JennyNeural",
2621
+ locale: "en-US",
2622
+ styles: [
2623
+ "assistant",
2624
+ "chat",
2625
+ "customerservice",
2626
+ "newscast",
2627
+ "cheerful",
2628
+ "empathetic",
2629
+ "excited",
2630
+ "friendly",
2631
+ "hopeful",
2632
+ "sad",
2633
+ "shouting",
2634
+ "terrified",
2635
+ "unfriendly",
2636
+ "whispering"
2637
+ ]
2638
+ },
2639
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
2640
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
2641
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2642
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2643
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
2644
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
2645
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
2646
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
2647
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
2648
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
2649
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
2650
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
2651
+ {
2652
+ name: "ru-RU-SvetlanaNeural",
2653
+ locale: "ru-RU",
2654
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
2655
+ },
2656
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
2657
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
2658
+ {
2659
+ name: "zh-CN-XiaoxiaoNeural",
2660
+ locale: "zh-CN",
2661
+ styles: [
2662
+ "assistant",
2663
+ "chat",
2664
+ "customerservice",
2665
+ "newscast",
2666
+ "cheerful",
2667
+ "empathetic",
2668
+ "excited",
2669
+ "friendly",
2670
+ "hopeful",
2671
+ "sad",
2672
+ "terrified",
2673
+ "whispering",
2674
+ "poetry-reading",
2675
+ "sports_commentary",
2676
+ "sports_commentary_excited",
2677
+ "story"
2678
+ ]
2679
+ },
2680
+ {
2681
+ name: "zh-CN-YunxiNeural",
2682
+ locale: "zh-CN",
2683
+ styles: [
2684
+ "narration-relaxed",
2685
+ "embarrassed",
2686
+ "fearful",
2687
+ "sad",
2688
+ "disgruntled",
2689
+ "serious",
2690
+ "angry",
2691
+ "depressed",
2692
+ "chat",
2693
+ "cheerful",
2694
+ "assistant"
2695
+ ]
2696
+ },
2697
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
2698
+ ];
2699
+ var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
2700
+ var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
2701
+ "characters",
2702
+ "spell-out",
2703
+ "cardinal",
2704
+ "ordinal",
2705
+ "number",
2706
+ "date",
2707
+ "time",
2708
+ "telephone",
2709
+ "fraction",
2710
+ "address",
2711
+ "name",
2712
+ "currency",
2713
+ "number_digit"
2714
+ ]);
2715
+ var ALLOWED_ROLES = /* @__PURE__ */ new Set([
2716
+ "Girl",
2717
+ "Boy",
2718
+ "YoungAdultFemale",
2719
+ "YoungAdultMale",
2720
+ "OlderAdultFemale",
2721
+ "OlderAdultMale",
2722
+ "SeniorFemale",
2723
+ "SeniorMale"
2724
+ ]);
2725
+ var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
2726
+ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
2727
+ "Leading",
2728
+ "Tailing",
2729
+ "Sentenceboundary",
2730
+ "Comma",
2731
+ "Semicolon",
2732
+ "Enumerationcomma"
2733
+ ]);
2734
+ var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
2735
+ function decodeAttribute(value) {
2736
+ return value.replace(
2737
+ /&(?:amp|apos|gt|lt|quot);/gi,
2738
+ (entity) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[entity.toLowerCase()] ?? entity
2739
+ );
2740
+ }
2741
+ function findTagEnd2(source, start) {
2742
+ let quote = "";
2743
+ for (let index = start; index < source.length; index += 1) {
2744
+ const character = source[index];
2745
+ if (quote) {
2746
+ if (character === quote) quote = "";
2747
+ } else if (character === '"' || character === "'") quote = character;
2748
+ else if (character === ">") return index;
2749
+ }
2750
+ return source.length - 1;
2751
+ }
2752
+ function tokenizeElements(source) {
2753
+ const tokens = [];
2754
+ const openElements = [];
2755
+ let index = 0;
2756
+ while (index < source.length) {
2757
+ const start = source.indexOf("<", index);
2758
+ if (start === -1) break;
2759
+ if (source.startsWith("<!--", start)) {
2760
+ const end2 = source.indexOf("-->", start + 4);
2761
+ index = end2 === -1 ? source.length : end2 + 3;
2762
+ continue;
2763
+ }
2764
+ if (source.startsWith("<![CDATA[", start)) {
2765
+ const end2 = source.indexOf("]]>", start + 9);
2766
+ index = end2 === -1 ? source.length : end2 + 3;
2767
+ continue;
2768
+ }
2769
+ if (source.startsWith("<?", start)) {
2770
+ const end2 = source.indexOf("?>", start + 2);
2771
+ index = end2 === -1 ? source.length : end2 + 2;
2772
+ continue;
2773
+ }
2774
+ const end = findTagEnd2(source, start + 1);
2775
+ const raw = source.slice(start, end + 1);
2776
+ if (raw.startsWith("</")) {
2777
+ openElements.pop();
2778
+ index = end + 1;
2779
+ continue;
2780
+ }
2781
+ const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
2782
+ if (!nameMatch?.[1]) {
2783
+ index = end + 1;
2784
+ continue;
2785
+ }
2786
+ const attributes = /* @__PURE__ */ new Map();
2787
+ const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
2788
+ const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
2789
+ for (const match of attributeSource.matchAll(attributePattern)) {
2790
+ attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
2791
+ }
2792
+ const selfClosing = /\/\s*>$/.test(raw);
2793
+ const parent = openElements[openElements.length - 1];
2794
+ const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2795
+ const tokenName = nameMatch[1];
2796
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2797
+ tokens.push({
2798
+ attributes,
2799
+ end,
2800
+ name: tokenName,
2801
+ parentName: parent?.name,
2802
+ parentVoiceName,
2803
+ selfClosing,
2804
+ start
2805
+ });
2806
+ if (!selfClosing) {
2807
+ openElements.push({
2808
+ name: tokenName,
2809
+ voiceName: tokenVoiceName
2810
+ });
2811
+ }
2812
+ index = end + 1;
2813
+ }
2814
+ return tokens;
2815
+ }
2816
+ function location(source, offset) {
2817
+ const before = source.slice(0, Math.max(0, offset));
2818
+ const line = before.split("\n").length;
2819
+ return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2820
+ }
2821
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
2822
+ diagnostics.push({
2823
+ ...location(source, offset),
2824
+ message,
2825
+ severity,
2826
+ source: "ssml-static-validator",
2827
+ ...code2 ? { code: code2 } : {}
2828
+ });
2829
+ }
2830
+ function isSupportedProsodyRate(value) {
2831
+ const trimmed = value.trim();
2832
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
2833
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
2834
+ if (!multiplier) return false;
2835
+ const numericValue = Number(multiplier[1]);
2836
+ return numericValue >= 0.5 && numericValue <= 2;
2837
+ }
2838
+ function isValidAzureAudioDuration(value) {
2839
+ const trimmed = value.trim();
2840
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
2841
+ if (numeric) return Number(numeric[1]) > 0;
2842
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
2843
+ if (!clock) return false;
2844
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2845
+ }
2846
+ function attr(token, name) {
2847
+ return token.attributes.get(name.toLowerCase());
2848
+ }
2849
+ var DEFAULT_LANGUAGE_ALIASES = {
2850
+ "zh-CN": ["zh-Hans"],
2851
+ "zh-TW": ["zh-Hant"]
2852
+ };
2853
+ function canonicalLanguageTag(language) {
2854
+ const trimmed = language.trim();
2855
+ if (!trimmed) return "";
2856
+ try {
2857
+ return new Intl.Locale(trimmed).toString().toLowerCase();
2858
+ } catch {
2859
+ return trimmed.toLowerCase();
2860
+ }
2861
+ }
2862
+ function createLanguageNormalizer(options) {
2863
+ const aliases = /* @__PURE__ */ new Map();
2864
+ const addAliasGroup = (canonical, values) => {
2865
+ const normalizedCanonical = canonicalLanguageTag(canonical);
2866
+ if (!normalizedCanonical) return;
2867
+ aliases.set(normalizedCanonical, normalizedCanonical);
2868
+ for (const value of values) {
2869
+ const normalizedValue = canonicalLanguageTag(value);
2870
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
2871
+ }
2872
+ };
2873
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
2874
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
2875
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
2876
+ return (language) => {
2877
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
2878
+ const normalized = canonicalLanguageTag(customValue);
2879
+ return aliases.get(normalized) ?? normalized;
2880
+ };
2881
+ }
2882
+ function voiceLocalePrefix(voiceName) {
2883
+ const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
2884
+ if (!match?.groups) return void 0;
2885
+ const tag = `${match.groups.language}-${match.groups.region}`;
2886
+ return {
2887
+ language: match.groups.language.toLowerCase(),
2888
+ region: match.groups.region.toLowerCase(),
2889
+ tag
2890
+ };
2891
+ }
2892
+ function definitionFromStyleMap(voiceName, styles) {
2893
+ return {
2894
+ name: voiceName,
2895
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
2896
+ styles
2897
+ };
2898
+ }
2899
+ function normalizeVoiceCatalog(options) {
2900
+ const definitions = /* @__PURE__ */ new Map();
2901
+ for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
2902
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
2903
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
2904
+ for (const definition of options.customVoiceDefinitions ?? [])
2905
+ definitions.set(definition.name.toLowerCase(), definition);
2906
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
2907
+ const key = voiceName.toLowerCase();
2908
+ const current = definitions.get(key);
2909
+ definitions.set(key, {
2910
+ ...current ?? definitionFromStyleMap(voiceName, styles),
2911
+ name: current?.name ?? voiceName,
2912
+ styles: styles.map((style) => style.toLowerCase())
2913
+ });
2914
+ }
2915
+ return definitions;
2916
+ }
2917
+ function diagnosticSeverity(policy) {
2918
+ if (policy === "ignore") return void 0;
2919
+ return policy === "error" ? "error" : "warning";
2920
+ }
2921
+ function languagePart(language) {
2922
+ try {
2923
+ return new Intl.Locale(language).language.toLowerCase();
2924
+ } catch {
2925
+ return language.split("-")[0]?.toLowerCase() ?? "";
2926
+ }
2927
+ }
2928
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
2929
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
2930
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
2931
+ const normalizedLanguage = normalizeLanguage(language);
2932
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
2933
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
2934
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
2935
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
2936
+ }
2937
+ function canonicalTagName(name) {
2938
+ const normalized = name.toLowerCase();
2939
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
2940
+ if (normalized === "sayas") return "say-as";
2941
+ return normalized;
2942
+ }
2943
+ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
2944
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
2945
+ return;
2946
+ const tagName = canonicalTagName(token.name);
2947
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
2948
+ const supportedTags = definition.supportedTags?.map(canonicalTagName);
2949
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
2950
+ addDiagnostic(
2951
+ diagnostics,
2952
+ source,
2953
+ token.start,
2954
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
2955
+ "error",
2956
+ "azure-unsupported-tag-for-voice"
2957
+ );
2958
+ }
2959
+ }
2960
+ function validateAudioSource(token, source, diagnostics, options, elementName2) {
2961
+ const src = attr(token, "src");
2962
+ if (!src) {
2963
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
2964
+ return;
2965
+ }
2966
+ let parsed;
2967
+ try {
2968
+ parsed = new URL(src);
2969
+ } catch {
2970
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
2971
+ return;
2972
+ }
2973
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2974
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2975
+ if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
2976
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2977
+ else if (!options.allowExternalAudio)
2978
+ addDiagnostic(
2979
+ diagnostics,
2980
+ source,
2981
+ token.start,
2982
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
2983
+ );
2984
+ }
2985
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
2986
+ const name = token.name.toLowerCase();
2987
+ if (name === "voice" && !attr(token, "name")?.trim())
2988
+ addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
2989
+ if (name === "break") {
2990
+ const time = attr(token, "time");
2991
+ const strength = attr(token, "strength");
2992
+ if (!time && !strength)
2993
+ addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
2994
+ if (time && strength)
2995
+ addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
2996
+ if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
2997
+ addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
2998
+ if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
2999
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
3000
+ }
3001
+ if (name === "prosody") {
3002
+ const rate = attr(token, "rate");
3003
+ const pitch = attr(token, "pitch");
3004
+ const volume = attr(token, "volume");
3005
+ if (rate && !isSupportedProsodyRate(rate))
3006
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
3007
+ if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
3008
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
3009
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
3010
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
3011
+ }
3012
+ if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
3013
+ const style = attr(token, "style");
3014
+ if (!style?.trim())
3015
+ addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
3016
+ const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
3017
+ if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
3018
+ addDiagnostic(
3019
+ diagnostics,
3020
+ source,
3021
+ token.start,
3022
+ "<mstts:express-as styledegree> must be a number between 0.01 and 2."
3023
+ );
3024
+ const role = attr(token, "role");
3025
+ if (role && !ALLOWED_ROLES.has(role))
3026
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
3027
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
3028
+ const supportedStyles = definition?.styles;
3029
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
3030
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
3031
+ addDiagnostic(
3032
+ diagnostics,
3033
+ source,
3034
+ token.start,
3035
+ `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
3036
+ severity,
3037
+ "azure-unsupported-style"
3038
+ );
3039
+ if (style && voiceName && !definition && severity)
3040
+ addDiagnostic(
3041
+ diagnostics,
3042
+ source,
3043
+ token.start,
3044
+ `Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
3045
+ severity
3046
+ );
3047
+ }
3048
+ if (name === "say-as" || name === "sayas") {
3049
+ const interpretAs = attr(token, "interpret-as");
3050
+ if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
3051
+ addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
3052
+ }
3053
+ if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
3054
+ addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
3055
+ if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
3056
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
3057
+ if (name === "sub" && !attr(token, "alias")?.trim())
3058
+ addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
3059
+ if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
3060
+ addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
3061
+ if (name === "mark" && !attr(token, "name")?.trim())
3062
+ addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
3063
+ if (name === "bookmark" && !attr(token, "mark")?.trim())
3064
+ addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
3065
+ if (name === "lexicon") {
3066
+ const uri = attr(token, "uri");
3067
+ if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
3068
+ else {
3069
+ try {
3070
+ const parsed = new URL(uri);
3071
+ if (parsed.protocol !== "https:")
3072
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
3073
+ } catch {
3074
+ addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
3075
+ }
3076
+ }
3077
+ }
3078
+ if (name === "mstts:silence") {
3079
+ const type = attr(token, "type");
3080
+ const value = attr(token, "value");
3081
+ if (!type || !ALLOWED_SILENCE_TYPES.has(type))
3082
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
3083
+ if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
3084
+ addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
3085
+ }
3086
+ if (name === "mstts:audioduration") {
3087
+ const value = attr(token, "value");
3088
+ if (!value || !isValidAzureAudioDuration(value))
3089
+ addDiagnostic(
3090
+ diagnostics,
3091
+ source,
3092
+ token.start,
3093
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
3094
+ );
3095
+ if (!token.selfClosing)
3096
+ addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
3097
+ }
3098
+ if (name === "mstts:viseme") {
3099
+ const type = attr(token, "type");
3100
+ if (!type || !ALLOWED_VISEME_TYPES.has(type))
3101
+ addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
3102
+ }
3103
+ if (name === "audio") {
3104
+ validateAudioSource(token, source, diagnostics, options, "audio");
3105
+ }
3106
+ if (name === "mstts:turn") {
3107
+ if (!attr(token, "voice")?.trim())
3108
+ addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
3109
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
3110
+ addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
3111
+ }
3112
+ if (name === "mstts:backgroundaudio") {
3113
+ validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
3114
+ const volume = attr(token, "volume");
3115
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
3116
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
3117
+ for (const [attribute, value] of [
3118
+ ["fadein", attr(token, "fadein")],
3119
+ ["fadeout", attr(token, "fadeout")]
3120
+ ]) {
3121
+ if (value && !isValidAzureAudioDuration(value))
3122
+ addDiagnostic(
3123
+ diagnostics,
3124
+ source,
3125
+ token.start,
3126
+ `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
3127
+ );
3128
+ }
3129
+ if (!token.selfClosing)
3130
+ addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3131
+ }
3132
+ }
3133
+ function validateAzureSsml(ssml, options = {}) {
3134
+ const diagnostics = [];
3135
+ if (typeof ssml !== "string") {
3136
+ return [
3137
+ {
3138
+ line: 1,
3139
+ column: 1,
3140
+ message: "SSML input must be a string",
3141
+ severity: "error",
3142
+ source: "ssml-static-validator"
3143
+ }
3144
+ ];
3145
+ }
3146
+ const maxLength = options.maxLength ?? 1e4;
3147
+ if (ssml.length > maxLength)
3148
+ addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
3149
+ try {
3150
+ parseSsml(ssml);
3151
+ } catch (error) {
3152
+ const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
3153
+ const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
3154
+ addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
3155
+ return diagnostics;
3156
+ }
3157
+ const tokens = tokenizeElements(ssml);
3158
+ const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
3159
+ const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
3160
+ if (!speak || voices.length === 0)
3161
+ addDiagnostic(
3162
+ diagnostics,
3163
+ ssml,
3164
+ speak?.start ?? 0,
3165
+ "Azure SSML requires at least one <voice> element under <speak>."
3166
+ );
3167
+ const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
3168
+ const voiceCatalog = normalizeVoiceCatalog(options);
3169
+ const normalizeLanguage = createLanguageNormalizer(options);
3170
+ const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
3171
+ const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
3172
+ for (const token of voicesToValidate) {
3173
+ const name = attr(token, "name")?.trim();
3174
+ const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
3175
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
3176
+ if (name && !definition && policySeverity)
3177
+ addDiagnostic(
3178
+ diagnostics,
3179
+ ssml,
3180
+ token.start,
3181
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
3182
+ policySeverity,
3183
+ "azure-unknown-voice"
3184
+ );
3185
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
3186
+ addDiagnostic(
3187
+ diagnostics,
3188
+ ssml,
3189
+ token.start,
3190
+ `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
3191
+ "warning",
3192
+ "azure-locale-mismatch"
3193
+ );
3194
+ }
3195
+ for (const token of tokens) {
3196
+ const tokenName = token.name.toLowerCase();
3197
+ const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
3198
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
3199
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
3200
+ validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
3201
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
3202
+ addDiagnostic(
3203
+ diagnostics,
3204
+ ssml,
3205
+ token.start,
3206
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
3207
+ "error",
3208
+ "azure-unsupported-model-for-voice"
3209
+ );
3210
+ }
3211
+ }
3212
+ return diagnostics;
3213
+ }
3214
+ var AZURE_VOICE_CATALOG_METADATA = {
3215
+ apiVersion: "2025-10-01",
3216
+ generatedAt: "2026-08-28T00:00:00.000Z",
3217
+ regions: [],
3218
+ voiceCount: AZURE_VOICE_DEFINITIONS.length
3219
+ };
2376
3220
 
2377
3221
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
2378
3222
  function getDocumentChildren(document2) {
@@ -2433,6 +3277,7 @@ var INTRINSICALLY_EMPTY_ELEMENTS = /* @__PURE__ */ new Set([
2433
3277
  "lexicon",
2434
3278
  "mark",
2435
3279
  "mstts:silence",
3280
+ "mstts:audioduration",
2436
3281
  "mstts:viseme",
2437
3282
  "silence",
2438
3283
  "viseme"
@@ -3130,7 +3975,7 @@ function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol =
3130
3975
  }
3131
3976
 
3132
3977
  // packages/ssml-editor-react/src/ssmlContext.ts
3133
- function findTagEnd2(source, start, limit) {
3978
+ function findTagEnd3(source, start, limit) {
3134
3979
  let quote;
3135
3980
  for (let index = start + 1; index < limit; index += 1) {
3136
3981
  const character = source[index];
@@ -3200,7 +4045,7 @@ function findActiveSsmlTags(source, offset) {
3200
4045
  index = end;
3201
4046
  continue;
3202
4047
  }
3203
- const tagEnd = findTagEnd2(source, tagStart, source.length);
4048
+ const tagEnd = findTagEnd3(source, tagStart, source.length);
3204
4049
  const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
3205
4050
  const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
3206
4051
  const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
@@ -3243,7 +4088,7 @@ function findSsmlVoiceContext(source, offset) {
3243
4088
  index = end === -1 || end + 2 > limit ? limit : end + 2;
3244
4089
  continue;
3245
4090
  }
3246
- const tagEnd = findTagEnd2(source, tagStart, limit);
4091
+ const tagEnd = findTagEnd3(source, tagStart, limit);
3247
4092
  if (tagEnd === -1) {
3248
4093
  break;
3249
4094
  }
@@ -3278,7 +4123,7 @@ function findSsmlVoiceContext(source, offset) {
3278
4123
  }
3279
4124
 
3280
4125
  // packages/ssml-editor-react/src/hooks/useSsmlEditorState.ts
3281
- var TIMING_INSERTION_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence"]);
4126
+ var TIMING_INSERTION_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
3282
4127
  var PROSODY_INSERTION_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
3283
4128
  var TEXT_INSERTION_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
3284
4129
  var EMPTY_SELECTION_OVERLAY = {
@@ -3999,6 +4844,28 @@ var SSML_COMPLETION_SNIPPETS = [
3999
4844
  label: "mstts:express-as",
4000
4845
  insertText: `<mstts:express-as style="cheerful">\${1:text}</mstts:express-as>`
4001
4846
  },
4847
+ {
4848
+ label: "mstts:audioduration",
4849
+ insertText: '<mstts:audioduration value="10s" />'
4850
+ },
4851
+ {
4852
+ label: "mstts:dialog",
4853
+ insertText: `<mstts:dialog>
4854
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
4855
+ </mstts:dialog>`
4856
+ },
4857
+ {
4858
+ label: "mstts:turn",
4859
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
4860
+ },
4861
+ {
4862
+ label: "mstts:backgroundaudio",
4863
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
4864
+ },
4865
+ {
4866
+ label: "mstts:ttsembedding",
4867
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
4868
+ },
4002
4869
  {
4003
4870
  label: "sub",
4004
4871
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -4177,7 +5044,7 @@ function registerSsmlCodeActions(monaco) {
4177
5044
 
4178
5045
  // packages/ssml-editor-react/src/ssmlCodeLens.ts
4179
5046
  var CODE_LENS_COMMAND = "ssml-editor.codeLens";
4180
- function findTagEnd3(source, start) {
5047
+ function findTagEnd4(source, start) {
4181
5048
  let quote;
4182
5049
  for (let index = start + 1; index < source.length; index += 1) {
4183
5050
  const character = source[index];
@@ -4217,7 +5084,7 @@ function getElementEnd(source, tagName, tagEnd) {
4217
5084
  index = index === -1 ? source.length : index + 3;
4218
5085
  continue;
4219
5086
  }
4220
- const nextEnd = findTagEnd3(source, nextStart);
5087
+ const nextEnd = findTagEnd4(source, nextStart);
4221
5088
  if (nextEnd === -1) {
4222
5089
  break;
4223
5090
  }
@@ -4269,12 +5136,12 @@ function registerSsmlCodeLens(monaco, editor, onOpenPopover) {
4269
5136
  index = index === -1 ? source.length : index + 3;
4270
5137
  continue;
4271
5138
  }
4272
- const tagEnd = findTagEnd3(source, tagStart);
5139
+ const tagEnd = findTagEnd4(source, tagStart);
4273
5140
  if (tagEnd === -1) {
4274
5141
  break;
4275
5142
  }
4276
5143
  const tag = source.slice(tagStart, tagEnd + 1);
4277
- const tagName = tag.match(/^<\s*(prosody|break)\b/i)?.[1]?.toLowerCase();
5144
+ const tagName = tag.match(/^<\s*(prosody|break|mstts:audioduration)\b/i)?.[1]?.toLowerCase();
4278
5145
  index = tagEnd + 1;
4279
5146
  if (!tagName) {
4280
5147
  continue;
@@ -4304,7 +5171,22 @@ function registerSsmlCodeLens(monaco, editor, onOpenPopover) {
4304
5171
  elementRange
4305
5172
  })
4306
5173
  );
4307
- } else if (/\/\s*>$/.test(tag)) {
5174
+ } else if (tagName === "mstts:audioduration" && /\/\s*>$/.test(tag)) {
5175
+ lenses.push(
5176
+ createLens(
5177
+ model,
5178
+ tagStart,
5179
+ tagEnd + 1,
5180
+ `\u26A1 Duration: ${getAttributeValue(tag, "value") ?? "default"} (Click to edit)`,
5181
+ { type: "attribute", insertionId: "mstts:audioduration", attributeName: "value", tagRange }
5182
+ ),
5183
+ createLens(model, tagStart, tagEnd + 1, "Delete", {
5184
+ type: "delete",
5185
+ tagRange,
5186
+ elementRange
5187
+ })
5188
+ );
5189
+ } else if (tagName === "break" && /\/\s*>$/.test(tag)) {
4308
5190
  lenses.push(
4309
5191
  createLens(
4310
5192
  model,
@@ -4640,6 +5522,74 @@ var SSML_TAG_DEFINITIONS = [
4640
5522
  values: VISEME_TYPE_PRESETS2
4641
5523
  }
4642
5524
  ]
5525
+ },
5526
+ {
5527
+ name: "mstts:audioduration",
5528
+ description: "Sets the target duration of synthesized audio.",
5529
+ parameters: [
5530
+ {
5531
+ name: "value",
5532
+ description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`.",
5533
+ example: "10s"
5534
+ }
5535
+ ]
5536
+ },
5537
+ {
5538
+ name: "mstts:dialog",
5539
+ description: "Groups multiple Azure dialog turns that can use different voices.",
5540
+ parameters: []
5541
+ },
5542
+ {
5543
+ name: "mstts:turn",
5544
+ description: "Adds one dialog turn using the required Azure voice name.",
5545
+ parameters: [
5546
+ {
5547
+ name: "voice",
5548
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
5549
+ example: "en-US-JennyNeural"
5550
+ }
5551
+ ]
5552
+ },
5553
+ {
5554
+ name: "mstts:backgroundaudio",
5555
+ description: "Plays background audio while speech is synthesized.",
5556
+ parameters: [
5557
+ {
5558
+ name: "src",
5559
+ description: "An absolute HTTP(S) URL for the background audio file.",
5560
+ example: "https://example.com/music.mp3"
5561
+ },
5562
+ {
5563
+ name: "volume",
5564
+ description: "The background audio volume, for example `-3dB` or `medium`.",
5565
+ example: "-3dB"
5566
+ },
5567
+ {
5568
+ name: "fadein",
5569
+ description: "The fade-in duration, for example `1s`.",
5570
+ example: "1s"
5571
+ },
5572
+ {
5573
+ name: "fadeout",
5574
+ description: "The fade-out duration, for example `500ms`.",
5575
+ example: "500ms"
5576
+ }
5577
+ ]
5578
+ },
5579
+ {
5580
+ name: "mstts:ttsembedding",
5581
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
5582
+ parameters: []
5583
+ },
5584
+ {
5585
+ name: "mstts:embedding",
5586
+ description: "Specifies embedding metadata for custom voice scenarios.",
5587
+ parameters: []
5588
+ },
5589
+ {
5590
+ name: "mstts:voiceconversion",
5591
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
5592
+ parameters: []
4643
5593
  }
4644
5594
  ];
4645
5595
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -4698,7 +5648,7 @@ function toRange(source, token) {
4698
5648
  function containsOffset(token, offset) {
4699
5649
  return offset >= token.start && offset < token.end;
4700
5650
  }
4701
- function findTagEnd4(source, start) {
5651
+ function findTagEnd5(source, start) {
4702
5652
  let quote;
4703
5653
  for (let index = start; index < source.length; index += 1) {
4704
5654
  const character = source[index];
@@ -4829,7 +5779,7 @@ function findTagAtOffset(source, offset) {
4829
5779
  searchStart = tokenEnd2;
4830
5780
  continue;
4831
5781
  }
4832
- const tagEnd = findTagEnd4(source, start + 1);
5782
+ const tagEnd = findTagEnd5(source, start + 1);
4833
5783
  const contentEnd = tagEnd ?? source.length;
4834
5784
  const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
4835
5785
  if (offset < tokenEnd) {
@@ -5244,9 +6194,143 @@ function useSsmlMonaco({
5244
6194
  return { editorRef, onMount };
5245
6195
  }
5246
6196
 
6197
+ // packages/ssml-editor-react/src/components/VisualSsmlEditor.tsx
6198
+ var import_react3 = require("react");
6199
+ function isElement(node) {
6200
+ return typeof node !== "string" && node.type !== "text";
6201
+ }
6202
+ function elementLabel(element) {
6203
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
6204
+ }
6205
+ function collectTextLeaves(nodes, path = [], ancestors = []) {
6206
+ return nodes.flatMap((node, index) => {
6207
+ const currentPath = [...path, index];
6208
+ if (typeof node === "string") return [{ ancestors, path: currentPath, value: node }];
6209
+ if (node.type === "text") return [{ ancestors, path: currentPath, value: node.value }];
6210
+ return collectTextLeaves(node.children ?? [], currentPath, [...ancestors, elementLabel(node)]);
6211
+ });
6212
+ }
6213
+ function updateNodesAtPath(nodes, path, update) {
6214
+ if (path.length === 0) return nodes;
6215
+ const [index, ...rest] = path;
6216
+ return nodes.flatMap((node, nodeIndex) => {
6217
+ if (nodeIndex !== index) return [node];
6218
+ if (rest.length === 0) {
6219
+ const next = update(node);
6220
+ return Array.isArray(next) ? next : [next];
6221
+ }
6222
+ if (!isElement(node)) return [node];
6223
+ return [{ ...node, children: updateNodesAtPath(node.children ?? [], rest, update) }];
6224
+ });
6225
+ }
6226
+ function updateTextAtPath(document2, path, value) {
6227
+ return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
6228
+ }
6229
+ function wrapTextAtPath(document2, path, start, end, tag, attributes) {
6230
+ return {
6231
+ ...document2,
6232
+ children: updateNodesAtPath(document2.children ?? [], path, (node) => {
6233
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
6234
+ if (!value || start === end) return node;
6235
+ if (tag === "break") {
6236
+ return [
6237
+ value.slice(0, start),
6238
+ { type: tag, attributes, children: [] },
6239
+ value.slice(start)
6240
+ ].filter((part) => typeof part === "string" ? part.length > 0 : true);
6241
+ }
6242
+ const selected = value.slice(start, end);
6243
+ const wrapped = { type: tag, attributes, children: [selected] };
6244
+ return [value.slice(0, start), wrapped, value.slice(end)].filter(
6245
+ (part) => typeof part === "string" ? part.length > 0 : true
6246
+ );
6247
+ })
6248
+ };
6249
+ }
6250
+ function TreeNode({
6251
+ node,
6252
+ path,
6253
+ selectedPath,
6254
+ onSelect
6255
+ }) {
6256
+ if (!isElement(node)) return null;
6257
+ const name = elementLabel(node);
6258
+ const isSelected = selectedPath?.join(".") === path.join(".");
6259
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`), (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
6260
+ TreeNode,
6261
+ {
6262
+ key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
6263
+ node: child,
6264
+ path: [...path, index],
6265
+ selectedPath,
6266
+ onSelect
6267
+ }
6268
+ ))));
6269
+ }
6270
+ function VisualSsmlEditor({
6271
+ document: document2,
6272
+ readOnly = false,
6273
+ onChange,
6274
+ onPreviewSelection
6275
+ }) {
6276
+ const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
6277
+ const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
6278
+ const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
6279
+ const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
6280
+ const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
6281
+ const commit = (nextDocument) => onChange?.(nextDocument);
6282
+ const applyWrapper = (tag, attributes) => {
6283
+ if (readOnly || !selectedLeaf) return;
6284
+ const start = selection.start === selection.end ? 0 : selection.start;
6285
+ const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
6286
+ commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
6287
+ };
6288
+ return /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual", "data-ssml-editor-visual": "" }, /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-breadcrumb" }, /* @__PURE__ */ React.createElement("legend", null, "SSML structure breadcrumb"), /* @__PURE__ */ React.createElement("span", null, "<speak>"), selectedLeaf?.ancestors.map((ancestor, index) => /* @__PURE__ */ React.createElement("span", { key: selectedLeaf?.ancestors.slice(0, index + 1).join("/") }, " / <", ancestor, ">")), selectedPath && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSelectedPath(null) }, "Clear parent")), diagnostics.length > 0 && /* @__PURE__ */ React.createElement("div", { role: "alert", "aria-invalid": "true", className: "ssml-editor-visual-errors" }, diagnostics.map((diagnostic) => /* @__PURE__ */ React.createElement("div", { key: `${diagnostic.code}-${diagnostic.message}` }, diagnostic.message))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-layout" }, /* @__PURE__ */ React.createElement("nav", { "aria-label": "SSML structure tree", className: "ssml-editor-visual-tree" }, /* @__PURE__ */ React.createElement("strong", null, "Structure"), /* @__PURE__ */ React.createElement("ul", null, document2.children?.map((node, index) => /* @__PURE__ */ React.createElement(
6289
+ TreeNode,
6290
+ {
6291
+ key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
6292
+ node,
6293
+ path: [index],
6294
+ selectedPath,
6295
+ onSelect: setSelectedPath
6296
+ }
6297
+ )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6298
+ "textarea",
6299
+ {
6300
+ value: selectedLeaf.value,
6301
+ readOnly,
6302
+ onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
6303
+ onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
6304
+ }
6305
+ )), /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-actions" }, /* @__PURE__ */ React.createElement("legend", null, "Apply SSML formatting"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { rate: "slow" }) }, "Rate"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { pitch: "high" }) }, "Pitch"), /* @__PURE__ */ React.createElement(
6306
+ "button",
6307
+ {
6308
+ type: "button",
6309
+ disabled: readOnly,
6310
+ onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
6311
+ },
6312
+ "Emotion"
6313
+ ), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
6314
+ "button",
6315
+ {
6316
+ type: "button",
6317
+ disabled: readOnly,
6318
+ onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
6319
+ },
6320
+ "Pronunciation"
6321
+ ), onPreviewSelection && /* @__PURE__ */ React.createElement(
6322
+ "button",
6323
+ {
6324
+ type: "button",
6325
+ onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
6326
+ },
6327
+ "Preview selection"
6328
+ ))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));
6329
+ }
6330
+
5247
6331
  // packages/ssml-editor-react/src/SsmlEditor.tsx
5248
6332
  var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
5249
- var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence"]);
6333
+ var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
5250
6334
  var PROSODY_POPOVER_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
5251
6335
  var TEXT_POPOVER_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
5252
6336
  function localizedText(value) {
@@ -5474,13 +6558,34 @@ var SSML_INSERTIONS = [
5474
6558
  suffix: "",
5475
6559
  mode: "insert"
5476
6560
  })
6561
+ },
6562
+ {
6563
+ id: "mstts:audioduration",
6564
+ icon: "\u25F7",
6565
+ tagName: "mstts:audioduration",
6566
+ selfClosing: true,
6567
+ labels: { ja: "\u97F3\u58F0\u9577", en: "Audio duration" },
6568
+ descriptions: {
6569
+ ja: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
6570
+ en: "Sets the target duration of synthesized audio."
6571
+ },
6572
+ parameterDescription: {
6573
+ ja: "\u76EE\u6A19\u6642\u9593\u3092\u9078\u629E\u3057\u307E\u3059\u3002",
6574
+ en: "Selects the target duration."
6575
+ },
6576
+ options: createInsertionOptions(AUDIO_DURATION_PRESETS, AUDIO_DURATION_DESCRIPTIONS),
6577
+ createTemplate: (value) => ({
6578
+ prefix: `<mstts:audioduration value="${value}"/>`,
6579
+ suffix: "",
6580
+ mode: "insert"
6581
+ })
5477
6582
  }
5478
6583
  ];
5479
6584
  var DEFAULT_INSERTION_GROUPS = [
5480
6585
  {
5481
6586
  id: "pauses",
5482
6587
  labels: { ja: "\u9593\u30FB\u7121\u97F3", en: "Pauses" },
5483
- insertionIds: ["break", "mstts:silence"]
6588
+ insertionIds: ["break", "mstts:silence", "mstts:audioduration"]
5484
6589
  },
5485
6590
  {
5486
6591
  id: "prosody",
@@ -5689,6 +6794,60 @@ var STYLE_CSS = `
5689
6794
  > .ssml-editor-help-settings-summary::before {
5690
6795
  content: "\u25BE";
5691
6796
  }
6797
+ [data-ssml-editor] .ssml-editor-visual-layout {
6798
+ display: grid;
6799
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
6800
+ gap: 1rem;
6801
+ }
6802
+ [data-ssml-editor] .ssml-editor-visual-tree,
6803
+ [data-ssml-editor] .ssml-editor-visual-form {
6804
+ display: grid;
6805
+ gap: 0.5rem;
6806
+ align-content: start;
6807
+ }
6808
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
6809
+ margin: 0;
6810
+ padding-left: 1.25rem;
6811
+ }
6812
+ [data-ssml-editor] .ssml-editor-visual-tree button,
6813
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb button,
6814
+ [data-ssml-editor] .ssml-editor-visual-actions button {
6815
+ padding: 0.35rem 0.5rem;
6816
+ border: 1px solid var(--ssml-editor-control-border);
6817
+ border-radius: 0.25rem;
6818
+ color: var(--ssml-editor-color);
6819
+ background: var(--ssml-editor-control-bg);
6820
+ cursor: pointer;
6821
+ }
6822
+ [data-ssml-editor] .ssml-editor-visual-tree button[aria-current] {
6823
+ border-color: var(--ssml-editor-active-border);
6824
+ background: var(--ssml-editor-active-bg);
6825
+ }
6826
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb,
6827
+ [data-ssml-editor] .ssml-editor-visual-actions {
6828
+ display: flex;
6829
+ flex-wrap: wrap;
6830
+ gap: 0.4rem;
6831
+ align-items: center;
6832
+ }
6833
+ [data-ssml-editor] .ssml-editor-visual-form textarea {
6834
+ box-sizing: border-box;
6835
+ width: 100%;
6836
+ min-height: 6rem;
6837
+ padding: 0.5rem;
6838
+ color: var(--ssml-editor-color);
6839
+ background: var(--ssml-editor-control-bg);
6840
+ border: 1px solid var(--ssml-editor-control-border);
6841
+ border-radius: 0.25rem;
6842
+ font: inherit;
6843
+ }
6844
+ [data-ssml-editor] .ssml-editor-visual-errors {
6845
+ padding: 0.5rem;
6846
+ color: var(--ssml-editor-error);
6847
+ background: var(--ssml-editor-error-bg);
6848
+ border: 1px solid var(--ssml-editor-error);
6849
+ border-radius: 0.25rem;
6850
+ }
5692
6851
  `.trim();
5693
6852
  function injectEditorTheme() {
5694
6853
  if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
@@ -5720,7 +6879,7 @@ function ToolbarDecorationsSwitch({
5720
6879
  /* @__PURE__ */ React.createElement("span", { className: "ssml-editor-switch-thumb", style: editorStyles.toolbarSwitchThumb, "aria-hidden": "true" })
5721
6880
  ));
5722
6881
  }
5723
- var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6882
+ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
5724
6883
  document: document2,
5725
6884
  onChange,
5726
6885
  onSsmlChange,
@@ -5758,8 +6917,11 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5758
6917
  toolbarStyle,
5759
6918
  displayClassName,
5760
6919
  displayStyle,
5761
- loadingFallback
6920
+ loadingFallback,
6921
+ editMode: editModeProp = "code"
5762
6922
  }, ref) {
6923
+ const [editMode, setEditMode] = (0, import_react4.useState)(editModeProp);
6924
+ (0, import_react4.useEffect)(() => setEditMode(editModeProp), [editModeProp]);
5763
6925
  const resolvedEditorOptions = {
5764
6926
  ...settings,
5765
6927
  ...editorOptions,
@@ -5837,6 +6999,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5837
6999
  );
5838
7000
  const {
5839
7001
  editorRef,
7002
+ draftDocument,
5840
7003
  text,
5841
7004
  selectionOverlay,
5842
7005
  activeTags,
@@ -5848,6 +7011,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5848
7011
  syntaxError,
5849
7012
  setSyntaxError,
5850
7013
  helpPanelId,
7014
+ commit,
5851
7015
  handleTextChange,
5852
7016
  handleInsert,
5853
7017
  handleInsertBreak,
@@ -5895,7 +7059,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
5895
7059
  enableCodeLens,
5896
7060
  onOpenPopover: handleCodeLensAction
5897
7061
  });
5898
- (0, import_react3.useImperativeHandle)(
7062
+ (0, import_react4.useImperativeHandle)(
5899
7063
  ref,
5900
7064
  () => ({
5901
7065
  getSelectedSsml: getSelectedSsml2,
@@ -6078,7 +7242,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6078
7242
  previousRenderedGroupId = groupId;
6079
7243
  hasRenderedToolbarItem = true;
6080
7244
  renderedItems.push(
6081
- /* @__PURE__ */ React.createElement(import_react3.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
7245
+ /* @__PURE__ */ React.createElement(import_react4.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
6082
7246
  );
6083
7247
  }
6084
7248
  return renderedItems;
@@ -6107,11 +7271,40 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6107
7271
  "aria-label": copy.toolbarAriaLabel,
6108
7272
  "data-ssml-editor-toolbar-actions": ""
6109
7273
  },
6110
- renderToolbarItems()
7274
+ renderToolbarItems(),
7275
+ /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }),
7276
+ /* @__PURE__ */ React.createElement(
7277
+ "button",
7278
+ {
7279
+ type: "button",
7280
+ style: editMode === "visual" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
7281
+ "aria-pressed": editMode === "visual",
7282
+ onClick: () => setEditMode("visual")
7283
+ },
7284
+ "Visual"
7285
+ ),
7286
+ /* @__PURE__ */ React.createElement(
7287
+ "button",
7288
+ {
7289
+ type: "button",
7290
+ style: editMode === "code" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
7291
+ "aria-pressed": editMode === "code",
7292
+ onClick: () => setEditMode("code")
7293
+ },
7294
+ "Code"
7295
+ )
6111
7296
  )
6112
7297
  ),
6113
- /* @__PURE__ */ React.createElement("div", { className: displayClassName, style: { ...editorStyles.display, ...displayStyle }, "data-ssml-editor-display": "" }, isHelpOpen && isSsmlEditorButtonVisible(buttonVisibility, "help") && /* @__PURE__ */ React.createElement("section", { id: helpPanelId, style: editorStyles.helpPanel, "aria-label": copy.helpHeading }, /* @__PURE__ */ React.createElement("h3", { style: editorStyles.helpHeading }, copy.helpHeading), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpDescription }, copy.helpDescription), helpInsertions.length > 0 && /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpList }, helpInsertions.map((insertion) => /* @__PURE__ */ React.createElement("li", { key: insertion.id, style: editorStyles.helpItem }, /* @__PURE__ */ React.createElement("details", { className: "ssml-editor-help-settings-accordion", style: editorStyles.helpSettingsAccordion }, /* @__PURE__ */ React.createElement("summary", { className: "ssml-editor-help-settings-summary", style: editorStyles.helpSettingsSummary }, /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpIcon, "aria-hidden": "true" }, insertion.icon), /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpSettingsSummaryContent }, /* @__PURE__ */ React.createElement("strong", null, insertion.labels[language]), " ", insertion.tagName && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("code", null, `<${insertion.tagName}${insertion.selfClosing ? "/>" : ">"}`), " \u2014", " "), insertion.descriptions[language])), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpSettingsDescription }, /* @__PURE__ */ React.createElement("strong", null, copy.parameters, ":"), " ", insertion.parameterDescription[language]), /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpSettingsList }, insertion.options.map((option) => /* @__PURE__ */ React.createElement("li", { key: option.value }, /* @__PURE__ */ React.createElement("strong", null, option.labels[language]), option.descriptions && /* @__PURE__ */ React.createElement(React.Fragment, null, " \u2014 ", option.descriptions[language]))))))))), /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
6114
- import_react4.default,
7298
+ /* @__PURE__ */ React.createElement("div", { className: displayClassName, style: { ...editorStyles.display, ...displayStyle }, "data-ssml-editor-display": "" }, isHelpOpen && isSsmlEditorButtonVisible(buttonVisibility, "help") && /* @__PURE__ */ React.createElement("section", { id: helpPanelId, style: editorStyles.helpPanel, "aria-label": copy.helpHeading }, /* @__PURE__ */ React.createElement("h3", { style: editorStyles.helpHeading }, copy.helpHeading), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpDescription }, copy.helpDescription), helpInsertions.length > 0 && /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpList }, helpInsertions.map((insertion) => /* @__PURE__ */ React.createElement("li", { key: insertion.id, style: editorStyles.helpItem }, /* @__PURE__ */ React.createElement("details", { className: "ssml-editor-help-settings-accordion", style: editorStyles.helpSettingsAccordion }, /* @__PURE__ */ React.createElement("summary", { className: "ssml-editor-help-settings-summary", style: editorStyles.helpSettingsSummary }, /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpIcon, "aria-hidden": "true" }, insertion.icon), /* @__PURE__ */ React.createElement("span", { style: editorStyles.helpSettingsSummaryContent }, /* @__PURE__ */ React.createElement("strong", null, insertion.labels[language]), " ", insertion.tagName && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("code", null, `<${insertion.tagName}${insertion.selfClosing ? "/>" : ">"}`), " \u2014", " "), insertion.descriptions[language])), /* @__PURE__ */ React.createElement("p", { style: editorStyles.helpSettingsDescription }, /* @__PURE__ */ React.createElement("strong", null, copy.parameters, ":"), " ", insertion.parameterDescription[language]), /* @__PURE__ */ React.createElement("ul", { style: editorStyles.helpSettingsList }, insertion.options.map((option) => /* @__PURE__ */ React.createElement("li", { key: option.value }, /* @__PURE__ */ React.createElement("strong", null, option.labels[language]), option.descriptions && /* @__PURE__ */ React.createElement(React.Fragment, null, " \u2014 ", option.descriptions[language]))))))))), editMode === "visual" ? /* @__PURE__ */ React.createElement("div", { style: editorStyles.visual }, /* @__PURE__ */ React.createElement(
7299
+ VisualSsmlEditor,
7300
+ {
7301
+ document: draftDocument,
7302
+ readOnly: isReadOnly,
7303
+ onChange: commit,
7304
+ onPreviewSelection
7305
+ }
7306
+ )) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
7307
+ import_react5.default,
6115
7308
  {
6116
7309
  height: editorHeight,
6117
7310
  language: "xml",
@@ -6172,6 +7365,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
6172
7365
  SSML_HOVER_COPY,
6173
7366
  SSML_INSERTIONS,
6174
7367
  SsmlEditor,
7368
+ VisualSsmlEditor,
6175
7369
  createSsmlEditorInsertionDefinition,
6176
7370
  registerSsmlCodeLens,
6177
7371
  updateTagAttribute