ssml-builder-js 2.8.1 → 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/README.md +80 -8
- package/dist/{chunk-VCDMKVVT.mjs → chunk-7LCSH4SZ.mjs} +457 -25
- package/dist/chunk-7LCSH4SZ.mjs.map +1 -0
- package/dist/{chunk-MWSDFGXW.mjs → chunk-JNJVTEL6.mjs} +785 -9
- package/dist/chunk-JNJVTEL6.mjs.map +1 -0
- package/dist/core.d.mts +91 -3
- package/dist/core.d.ts +91 -3
- package/dist/core.js +462 -25
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +13 -3
- package/dist/elements.d.mts +10 -1
- package/dist/elements.d.ts +10 -1
- package/dist/elements.js +1071 -13
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +292 -8
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +462 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +40 -3
- package/dist/react.d.ts +40 -3
- package/dist/react.js +1026 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +237 -5
- package/dist/react.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-MWSDFGXW.mjs.map +0 -1
- package/dist/chunk-VCDMKVVT.mjs.map +0 -1
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
|
|
47
|
-
var
|
|
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) {
|
|
@@ -473,6 +474,11 @@ var SSML_ATTRIBUTE_PRESETS = {
|
|
|
473
474
|
"mstts:audioduration": {
|
|
474
475
|
value: AUDIO_DURATION_PRESETS
|
|
475
476
|
},
|
|
477
|
+
"mstts:backgroundaudio": {
|
|
478
|
+
volume: PROSODY_VOLUME_PRESETS,
|
|
479
|
+
fadein: AUDIO_DURATION_PRESETS,
|
|
480
|
+
fadeout: AUDIO_DURATION_PRESETS
|
|
481
|
+
},
|
|
476
482
|
silence: {
|
|
477
483
|
type: SILENCE_TYPE_PRESETS,
|
|
478
484
|
value: SILENCE_VALUE_PRESETS
|
|
@@ -1289,6 +1295,14 @@ var editorStyles = {
|
|
|
1289
1295
|
display: "grid",
|
|
1290
1296
|
gap: "0.5rem"
|
|
1291
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
|
+
},
|
|
1292
1306
|
toolbarIconOnly: {
|
|
1293
1307
|
justifyContent: "center",
|
|
1294
1308
|
minWidth: "2.25rem",
|
|
@@ -1694,7 +1708,13 @@ var SSML_TAGS = {
|
|
|
1694
1708
|
SILENCE: "silence",
|
|
1695
1709
|
MSTTS_VISEME: "mstts:viseme",
|
|
1696
1710
|
VISEME: "viseme",
|
|
1697
|
-
MSTTS_AUDIO_DURATION: "mstts:audioduration"
|
|
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"
|
|
1698
1718
|
};
|
|
1699
1719
|
var SSML_ATTRS = {
|
|
1700
1720
|
VERSION: "version",
|
|
@@ -1703,6 +1723,7 @@ var SSML_ATTRS = {
|
|
|
1703
1723
|
LANG: "lang",
|
|
1704
1724
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
1705
1725
|
NAME: "name",
|
|
1726
|
+
VOICE: "voice",
|
|
1706
1727
|
EFFECT: "effect",
|
|
1707
1728
|
RATE: "rate",
|
|
1708
1729
|
PITCH: "pitch",
|
|
@@ -1734,7 +1755,9 @@ var SSML_ATTRS = {
|
|
|
1734
1755
|
MARK: "mark",
|
|
1735
1756
|
URI: "uri",
|
|
1736
1757
|
TYPE: "type",
|
|
1737
|
-
VALUE: "value"
|
|
1758
|
+
VALUE: "value",
|
|
1759
|
+
FADE_IN: "fadein",
|
|
1760
|
+
FADE_OUT: "fadeout"
|
|
1738
1761
|
};
|
|
1739
1762
|
function escapeText(value) {
|
|
1740
1763
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1824,6 +1847,20 @@ function getAttributes(element) {
|
|
|
1824
1847
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
1825
1848
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
1826
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;
|
|
1827
1864
|
case SSML_TAGS.PARAGRAPH:
|
|
1828
1865
|
case SSML_TAGS.SENTENCE:
|
|
1829
1866
|
case SSML_TAGS.WORD:
|
|
@@ -2388,6 +2425,40 @@ function convertElement(node) {
|
|
|
2388
2425
|
if (value !== void 0) element.value = value;
|
|
2389
2426
|
return finishElement(element, node, attributes);
|
|
2390
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
|
+
}
|
|
2391
2462
|
default: {
|
|
2392
2463
|
const element = {
|
|
2393
2464
|
name: node.name,
|
|
@@ -2508,6 +2579,644 @@ function validateSsml(xmlString) {
|
|
|
2508
2579
|
};
|
|
2509
2580
|
}
|
|
2510
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) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[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
|
+
};
|
|
2511
3220
|
|
|
2512
3221
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
2513
3222
|
function getDocumentChildren(document2) {
|
|
@@ -3266,7 +3975,7 @@ function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol =
|
|
|
3266
3975
|
}
|
|
3267
3976
|
|
|
3268
3977
|
// packages/ssml-editor-react/src/ssmlContext.ts
|
|
3269
|
-
function
|
|
3978
|
+
function findTagEnd3(source, start, limit) {
|
|
3270
3979
|
let quote;
|
|
3271
3980
|
for (let index = start + 1; index < limit; index += 1) {
|
|
3272
3981
|
const character = source[index];
|
|
@@ -3336,7 +4045,7 @@ function findActiveSsmlTags(source, offset) {
|
|
|
3336
4045
|
index = end;
|
|
3337
4046
|
continue;
|
|
3338
4047
|
}
|
|
3339
|
-
const tagEnd =
|
|
4048
|
+
const tagEnd = findTagEnd3(source, tagStart, source.length);
|
|
3340
4049
|
const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
|
|
3341
4050
|
const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
3342
4051
|
const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
@@ -3379,7 +4088,7 @@ function findSsmlVoiceContext(source, offset) {
|
|
|
3379
4088
|
index = end === -1 || end + 2 > limit ? limit : end + 2;
|
|
3380
4089
|
continue;
|
|
3381
4090
|
}
|
|
3382
|
-
const tagEnd =
|
|
4091
|
+
const tagEnd = findTagEnd3(source, tagStart, limit);
|
|
3383
4092
|
if (tagEnd === -1) {
|
|
3384
4093
|
break;
|
|
3385
4094
|
}
|
|
@@ -4139,6 +4848,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
4139
4848
|
label: "mstts:audioduration",
|
|
4140
4849
|
insertText: '<mstts:audioduration value="10s" />'
|
|
4141
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
|
+
},
|
|
4142
4869
|
{
|
|
4143
4870
|
label: "sub",
|
|
4144
4871
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -4317,7 +5044,7 @@ function registerSsmlCodeActions(monaco) {
|
|
|
4317
5044
|
|
|
4318
5045
|
// packages/ssml-editor-react/src/ssmlCodeLens.ts
|
|
4319
5046
|
var CODE_LENS_COMMAND = "ssml-editor.codeLens";
|
|
4320
|
-
function
|
|
5047
|
+
function findTagEnd4(source, start) {
|
|
4321
5048
|
let quote;
|
|
4322
5049
|
for (let index = start + 1; index < source.length; index += 1) {
|
|
4323
5050
|
const character = source[index];
|
|
@@ -4357,7 +5084,7 @@ function getElementEnd(source, tagName, tagEnd) {
|
|
|
4357
5084
|
index = index === -1 ? source.length : index + 3;
|
|
4358
5085
|
continue;
|
|
4359
5086
|
}
|
|
4360
|
-
const nextEnd =
|
|
5087
|
+
const nextEnd = findTagEnd4(source, nextStart);
|
|
4361
5088
|
if (nextEnd === -1) {
|
|
4362
5089
|
break;
|
|
4363
5090
|
}
|
|
@@ -4409,7 +5136,7 @@ function registerSsmlCodeLens(monaco, editor, onOpenPopover) {
|
|
|
4409
5136
|
index = index === -1 ? source.length : index + 3;
|
|
4410
5137
|
continue;
|
|
4411
5138
|
}
|
|
4412
|
-
const tagEnd =
|
|
5139
|
+
const tagEnd = findTagEnd4(source, tagStart);
|
|
4413
5140
|
if (tagEnd === -1) {
|
|
4414
5141
|
break;
|
|
4415
5142
|
}
|
|
@@ -4806,6 +5533,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
4806
5533
|
example: "10s"
|
|
4807
5534
|
}
|
|
4808
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: []
|
|
4809
5593
|
}
|
|
4810
5594
|
];
|
|
4811
5595
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -4864,7 +5648,7 @@ function toRange(source, token) {
|
|
|
4864
5648
|
function containsOffset(token, offset) {
|
|
4865
5649
|
return offset >= token.start && offset < token.end;
|
|
4866
5650
|
}
|
|
4867
|
-
function
|
|
5651
|
+
function findTagEnd5(source, start) {
|
|
4868
5652
|
let quote;
|
|
4869
5653
|
for (let index = start; index < source.length; index += 1) {
|
|
4870
5654
|
const character = source[index];
|
|
@@ -4995,7 +5779,7 @@ function findTagAtOffset(source, offset) {
|
|
|
4995
5779
|
searchStart = tokenEnd2;
|
|
4996
5780
|
continue;
|
|
4997
5781
|
}
|
|
4998
|
-
const tagEnd =
|
|
5782
|
+
const tagEnd = findTagEnd5(source, start + 1);
|
|
4999
5783
|
const contentEnd = tagEnd ?? source.length;
|
|
5000
5784
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
5001
5785
|
if (offset < tokenEnd) {
|
|
@@ -5410,6 +6194,140 @@ function useSsmlMonaco({
|
|
|
5410
6194
|
return { editorRef, onMount };
|
|
5411
6195
|
}
|
|
5412
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
|
+
|
|
5413
6331
|
// packages/ssml-editor-react/src/SsmlEditor.tsx
|
|
5414
6332
|
var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
|
|
5415
6333
|
var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
|
|
@@ -5876,6 +6794,60 @@ var STYLE_CSS = `
|
|
|
5876
6794
|
> .ssml-editor-help-settings-summary::before {
|
|
5877
6795
|
content: "\u25BE";
|
|
5878
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
|
+
}
|
|
5879
6851
|
`.trim();
|
|
5880
6852
|
function injectEditorTheme() {
|
|
5881
6853
|
if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
|
|
@@ -5907,7 +6879,7 @@ function ToolbarDecorationsSwitch({
|
|
|
5907
6879
|
/* @__PURE__ */ React.createElement("span", { className: "ssml-editor-switch-thumb", style: editorStyles.toolbarSwitchThumb, "aria-hidden": "true" })
|
|
5908
6880
|
));
|
|
5909
6881
|
}
|
|
5910
|
-
var SsmlEditor = (0,
|
|
6882
|
+
var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
|
|
5911
6883
|
document: document2,
|
|
5912
6884
|
onChange,
|
|
5913
6885
|
onSsmlChange,
|
|
@@ -5945,8 +6917,11 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
5945
6917
|
toolbarStyle,
|
|
5946
6918
|
displayClassName,
|
|
5947
6919
|
displayStyle,
|
|
5948
|
-
loadingFallback
|
|
6920
|
+
loadingFallback,
|
|
6921
|
+
editMode: editModeProp = "code"
|
|
5949
6922
|
}, ref) {
|
|
6923
|
+
const [editMode, setEditMode] = (0, import_react4.useState)(editModeProp);
|
|
6924
|
+
(0, import_react4.useEffect)(() => setEditMode(editModeProp), [editModeProp]);
|
|
5950
6925
|
const resolvedEditorOptions = {
|
|
5951
6926
|
...settings,
|
|
5952
6927
|
...editorOptions,
|
|
@@ -6024,6 +6999,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6024
6999
|
);
|
|
6025
7000
|
const {
|
|
6026
7001
|
editorRef,
|
|
7002
|
+
draftDocument,
|
|
6027
7003
|
text,
|
|
6028
7004
|
selectionOverlay,
|
|
6029
7005
|
activeTags,
|
|
@@ -6035,6 +7011,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6035
7011
|
syntaxError,
|
|
6036
7012
|
setSyntaxError,
|
|
6037
7013
|
helpPanelId,
|
|
7014
|
+
commit,
|
|
6038
7015
|
handleTextChange,
|
|
6039
7016
|
handleInsert,
|
|
6040
7017
|
handleInsertBreak,
|
|
@@ -6082,7 +7059,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6082
7059
|
enableCodeLens,
|
|
6083
7060
|
onOpenPopover: handleCodeLensAction
|
|
6084
7061
|
});
|
|
6085
|
-
(0,
|
|
7062
|
+
(0, import_react4.useImperativeHandle)(
|
|
6086
7063
|
ref,
|
|
6087
7064
|
() => ({
|
|
6088
7065
|
getSelectedSsml: getSelectedSsml2,
|
|
@@ -6265,7 +7242,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6265
7242
|
previousRenderedGroupId = groupId;
|
|
6266
7243
|
hasRenderedToolbarItem = true;
|
|
6267
7244
|
renderedItems.push(
|
|
6268
|
-
/* @__PURE__ */ React.createElement(
|
|
7245
|
+
/* @__PURE__ */ React.createElement(import_react4.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
|
|
6269
7246
|
);
|
|
6270
7247
|
}
|
|
6271
7248
|
return renderedItems;
|
|
@@ -6294,11 +7271,40 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6294
7271
|
"aria-label": copy.toolbarAriaLabel,
|
|
6295
7272
|
"data-ssml-editor-toolbar-actions": ""
|
|
6296
7273
|
},
|
|
6297
|
-
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
|
+
)
|
|
6298
7296
|
)
|
|
6299
7297
|
),
|
|
6300
|
-
/* @__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:
|
|
6301
|
-
|
|
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,
|
|
6302
7308
|
{
|
|
6303
7309
|
height: editorHeight,
|
|
6304
7310
|
language: "xml",
|
|
@@ -6359,6 +7365,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6359
7365
|
SSML_HOVER_COPY,
|
|
6360
7366
|
SSML_INSERTIONS,
|
|
6361
7367
|
SsmlEditor,
|
|
7368
|
+
VisualSsmlEditor,
|
|
6362
7369
|
createSsmlEditorInsertionDefinition,
|
|
6363
7370
|
registerSsmlCodeLens,
|
|
6364
7371
|
updateTagAttribute
|