ssml-builder-js 2.8.1 → 2.10.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 +88 -12
- package/dist/{chunk-MWSDFGXW.mjs → chunk-BYIZQL2W.mjs} +854 -9
- package/dist/chunk-BYIZQL2W.mjs.map +1 -0
- package/dist/{chunk-VCDMKVVT.mjs → chunk-GW6CKHXF.mjs} +539 -25
- package/dist/chunk-GW6CKHXF.mjs.map +1 -0
- package/dist/core.d.mts +98 -3
- package/dist/core.d.ts +98 -3
- package/dist/core.js +544 -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 +1140 -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 +544 -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 +47 -3
- package/dist/react.d.ts +47 -3
- package/dist/react.js +1095 -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,8 @@ var SSML_ATTRS = {
|
|
|
1703
1723
|
LANG: "lang",
|
|
1704
1724
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
1705
1725
|
NAME: "name",
|
|
1726
|
+
VOICE: "voice",
|
|
1727
|
+
SPEAKER: "speaker",
|
|
1706
1728
|
EFFECT: "effect",
|
|
1707
1729
|
RATE: "rate",
|
|
1708
1730
|
PITCH: "pitch",
|
|
@@ -1733,8 +1755,15 @@ var SSML_ATTRS = {
|
|
|
1733
1755
|
ALIAS: "alias",
|
|
1734
1756
|
MARK: "mark",
|
|
1735
1757
|
URI: "uri",
|
|
1758
|
+
ID: "id",
|
|
1759
|
+
MODEL: "model",
|
|
1760
|
+
PROFILE: "profile",
|
|
1761
|
+
URL: "url",
|
|
1762
|
+
SPEAKER_PROFILE_ID: "speakerProfileId",
|
|
1736
1763
|
TYPE: "type",
|
|
1737
|
-
VALUE: "value"
|
|
1764
|
+
VALUE: "value",
|
|
1765
|
+
FADE_IN: "fadein",
|
|
1766
|
+
FADE_OUT: "fadeout"
|
|
1738
1767
|
};
|
|
1739
1768
|
function escapeText(value) {
|
|
1740
1769
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1824,6 +1853,30 @@ function getAttributes(element) {
|
|
|
1824
1853
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
1825
1854
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
1826
1855
|
break;
|
|
1856
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
1857
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
1858
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
|
|
1859
|
+
break;
|
|
1860
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
1861
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
1862
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
1863
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
1864
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
1865
|
+
break;
|
|
1866
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
1867
|
+
break;
|
|
1868
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
1869
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1870
|
+
break;
|
|
1871
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
1872
|
+
addAttribute(attributes, SSML_ATTRS.ID, element.id);
|
|
1873
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1874
|
+
break;
|
|
1875
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
1876
|
+
addAttribute(attributes, SSML_ATTRS.URL, element.url);
|
|
1877
|
+
addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
|
|
1878
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1879
|
+
break;
|
|
1827
1880
|
case SSML_TAGS.PARAGRAPH:
|
|
1828
1881
|
case SSML_TAGS.SENTENCE:
|
|
1829
1882
|
case SSML_TAGS.WORD:
|
|
@@ -2388,6 +2441,54 @@ function convertElement(node) {
|
|
|
2388
2441
|
if (value !== void 0) element.value = value;
|
|
2389
2442
|
return finishElement(element, node, attributes);
|
|
2390
2443
|
}
|
|
2444
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
2445
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
2446
|
+
return finishElement(element, node, attributes);
|
|
2447
|
+
}
|
|
2448
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
2449
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
2450
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
2451
|
+
const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
|
|
2452
|
+
if (voice !== void 0) element.voice = voice;
|
|
2453
|
+
if (speaker !== void 0) element.speaker = speaker;
|
|
2454
|
+
return finishElement(element, node, attributes);
|
|
2455
|
+
}
|
|
2456
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
2457
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
2458
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
2459
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
2460
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
2461
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
2462
|
+
if (src !== void 0) element.src = src;
|
|
2463
|
+
if (volume !== void 0) element.volume = volume;
|
|
2464
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
2465
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
2466
|
+
return finishElement(element, node, attributes);
|
|
2467
|
+
}
|
|
2468
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
2469
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
2470
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2471
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2472
|
+
return finishElement(element, node, attributes);
|
|
2473
|
+
}
|
|
2474
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
2475
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
2476
|
+
const id = readAttribute(attributes, SSML_ATTRS.ID);
|
|
2477
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2478
|
+
if (id !== void 0) element.id = id;
|
|
2479
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2480
|
+
return finishElement(element, node, attributes);
|
|
2481
|
+
}
|
|
2482
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
2483
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
2484
|
+
const url = readAttribute(attributes, SSML_ATTRS.URL);
|
|
2485
|
+
const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
|
|
2486
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2487
|
+
if (url !== void 0) element.url = url;
|
|
2488
|
+
if (profile !== void 0) element.profile = profile;
|
|
2489
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2490
|
+
return finishElement(element, node, attributes);
|
|
2491
|
+
}
|
|
2391
2492
|
default: {
|
|
2392
2493
|
const element = {
|
|
2393
2494
|
name: node.name,
|
|
@@ -2508,6 +2609,683 @@ function validateSsml(xmlString) {
|
|
|
2508
2609
|
};
|
|
2509
2610
|
}
|
|
2510
2611
|
}
|
|
2612
|
+
var AZURE_VOICE_DEFINITIONS = [
|
|
2613
|
+
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2614
|
+
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2615
|
+
{ name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
|
|
2616
|
+
{
|
|
2617
|
+
name: "en-US-GuyNeural",
|
|
2618
|
+
locale: "en-US",
|
|
2619
|
+
styles: [
|
|
2620
|
+
"angry",
|
|
2621
|
+
"cheerful",
|
|
2622
|
+
"excited",
|
|
2623
|
+
"friendly",
|
|
2624
|
+
"hopeful",
|
|
2625
|
+
"newscast",
|
|
2626
|
+
"sad",
|
|
2627
|
+
"shouting",
|
|
2628
|
+
"terrified",
|
|
2629
|
+
"unfriendly",
|
|
2630
|
+
"whispering"
|
|
2631
|
+
]
|
|
2632
|
+
},
|
|
2633
|
+
{
|
|
2634
|
+
name: "en-US-JennyMultilingualNeural",
|
|
2635
|
+
locale: "en-US",
|
|
2636
|
+
styles: [
|
|
2637
|
+
"cheerful",
|
|
2638
|
+
"empathetic",
|
|
2639
|
+
"excited",
|
|
2640
|
+
"friendly",
|
|
2641
|
+
"hopeful",
|
|
2642
|
+
"sad",
|
|
2643
|
+
"shouting",
|
|
2644
|
+
"terrified",
|
|
2645
|
+
"unfriendly",
|
|
2646
|
+
"whispering"
|
|
2647
|
+
]
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
name: "en-US-JennyNeural",
|
|
2651
|
+
locale: "en-US",
|
|
2652
|
+
styles: [
|
|
2653
|
+
"assistant",
|
|
2654
|
+
"chat",
|
|
2655
|
+
"customerservice",
|
|
2656
|
+
"newscast",
|
|
2657
|
+
"cheerful",
|
|
2658
|
+
"empathetic",
|
|
2659
|
+
"excited",
|
|
2660
|
+
"friendly",
|
|
2661
|
+
"hopeful",
|
|
2662
|
+
"sad",
|
|
2663
|
+
"shouting",
|
|
2664
|
+
"terrified",
|
|
2665
|
+
"unfriendly",
|
|
2666
|
+
"whispering"
|
|
2667
|
+
]
|
|
2668
|
+
},
|
|
2669
|
+
{ name: "es-ES-ElviraNeural", locale: "es-ES" },
|
|
2670
|
+
{ name: "fil-PH-AngeloNeural", locale: "fil-PH" },
|
|
2671
|
+
{ name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2672
|
+
{ name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2673
|
+
{ name: "id-ID-GadisNeural", locale: "id-ID" },
|
|
2674
|
+
{ name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
|
|
2675
|
+
{ name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
|
|
2676
|
+
{ name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
|
|
2677
|
+
{ name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
|
|
2678
|
+
{ name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
|
|
2679
|
+
{ name: "ms-MY-YasminNeural", locale: "ms-MY" },
|
|
2680
|
+
{ name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
|
|
2681
|
+
{
|
|
2682
|
+
name: "ru-RU-SvetlanaNeural",
|
|
2683
|
+
locale: "ru-RU",
|
|
2684
|
+
styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
2685
|
+
},
|
|
2686
|
+
{ name: "th-TH-PremwadeeNeural", locale: "th-TH" },
|
|
2687
|
+
{ name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
|
|
2688
|
+
{
|
|
2689
|
+
name: "zh-CN-XiaoxiaoNeural",
|
|
2690
|
+
locale: "zh-CN",
|
|
2691
|
+
styles: [
|
|
2692
|
+
"assistant",
|
|
2693
|
+
"chat",
|
|
2694
|
+
"customerservice",
|
|
2695
|
+
"newscast",
|
|
2696
|
+
"cheerful",
|
|
2697
|
+
"empathetic",
|
|
2698
|
+
"excited",
|
|
2699
|
+
"friendly",
|
|
2700
|
+
"hopeful",
|
|
2701
|
+
"sad",
|
|
2702
|
+
"terrified",
|
|
2703
|
+
"whispering",
|
|
2704
|
+
"poetry-reading",
|
|
2705
|
+
"sports_commentary",
|
|
2706
|
+
"sports_commentary_excited",
|
|
2707
|
+
"story"
|
|
2708
|
+
]
|
|
2709
|
+
},
|
|
2710
|
+
{
|
|
2711
|
+
name: "zh-CN-YunxiNeural",
|
|
2712
|
+
locale: "zh-CN",
|
|
2713
|
+
styles: [
|
|
2714
|
+
"narration-relaxed",
|
|
2715
|
+
"embarrassed",
|
|
2716
|
+
"fearful",
|
|
2717
|
+
"sad",
|
|
2718
|
+
"disgruntled",
|
|
2719
|
+
"serious",
|
|
2720
|
+
"angry",
|
|
2721
|
+
"depressed",
|
|
2722
|
+
"chat",
|
|
2723
|
+
"cheerful",
|
|
2724
|
+
"assistant"
|
|
2725
|
+
]
|
|
2726
|
+
},
|
|
2727
|
+
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
2728
|
+
];
|
|
2729
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
2730
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
2731
|
+
"characters",
|
|
2732
|
+
"spell-out",
|
|
2733
|
+
"cardinal",
|
|
2734
|
+
"ordinal",
|
|
2735
|
+
"number",
|
|
2736
|
+
"date",
|
|
2737
|
+
"time",
|
|
2738
|
+
"telephone",
|
|
2739
|
+
"fraction",
|
|
2740
|
+
"address",
|
|
2741
|
+
"name",
|
|
2742
|
+
"currency",
|
|
2743
|
+
"number_digit"
|
|
2744
|
+
]);
|
|
2745
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
2746
|
+
"Girl",
|
|
2747
|
+
"Boy",
|
|
2748
|
+
"YoungAdultFemale",
|
|
2749
|
+
"YoungAdultMale",
|
|
2750
|
+
"OlderAdultFemale",
|
|
2751
|
+
"OlderAdultMale",
|
|
2752
|
+
"SeniorFemale",
|
|
2753
|
+
"SeniorMale"
|
|
2754
|
+
]);
|
|
2755
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
2756
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
2757
|
+
"Leading",
|
|
2758
|
+
"Tailing",
|
|
2759
|
+
"Sentenceboundary",
|
|
2760
|
+
"Comma",
|
|
2761
|
+
"Semicolon",
|
|
2762
|
+
"Enumerationcomma"
|
|
2763
|
+
]);
|
|
2764
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
2765
|
+
function decodeAttribute(value) {
|
|
2766
|
+
return value.replace(
|
|
2767
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
2768
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
function findTagEnd2(source, start) {
|
|
2772
|
+
let quote = "";
|
|
2773
|
+
for (let index = start; index < source.length; index += 1) {
|
|
2774
|
+
const character = source[index];
|
|
2775
|
+
if (quote) {
|
|
2776
|
+
if (character === quote) quote = "";
|
|
2777
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
2778
|
+
else if (character === ">") return index;
|
|
2779
|
+
}
|
|
2780
|
+
return source.length - 1;
|
|
2781
|
+
}
|
|
2782
|
+
function tokenizeElements(source) {
|
|
2783
|
+
const tokens = [];
|
|
2784
|
+
const openElements = [];
|
|
2785
|
+
let index = 0;
|
|
2786
|
+
while (index < source.length) {
|
|
2787
|
+
const start = source.indexOf("<", index);
|
|
2788
|
+
if (start === -1) break;
|
|
2789
|
+
if (source.startsWith("<!--", start)) {
|
|
2790
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
2791
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
2795
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
2796
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (source.startsWith("<?", start)) {
|
|
2800
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
2801
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const end = findTagEnd2(source, start + 1);
|
|
2805
|
+
const raw = source.slice(start, end + 1);
|
|
2806
|
+
if (raw.startsWith("</")) {
|
|
2807
|
+
openElements.pop();
|
|
2808
|
+
index = end + 1;
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
2812
|
+
if (!nameMatch?.[1]) {
|
|
2813
|
+
index = end + 1;
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
2817
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
2818
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
2819
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
2820
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
2821
|
+
}
|
|
2822
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
2823
|
+
const parent = openElements[openElements.length - 1];
|
|
2824
|
+
const childElementIndex = parent?.childElementCount;
|
|
2825
|
+
if (parent) parent.childElementCount += 1;
|
|
2826
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
2827
|
+
const tokenName = nameMatch[1];
|
|
2828
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
2829
|
+
tokens.push({
|
|
2830
|
+
attributes,
|
|
2831
|
+
childElementIndex,
|
|
2832
|
+
end,
|
|
2833
|
+
name: tokenName,
|
|
2834
|
+
parentName: parent?.name,
|
|
2835
|
+
parentVoiceName,
|
|
2836
|
+
selfClosing,
|
|
2837
|
+
start
|
|
2838
|
+
});
|
|
2839
|
+
if (!selfClosing) {
|
|
2840
|
+
openElements.push({
|
|
2841
|
+
childElementCount: 0,
|
|
2842
|
+
name: tokenName,
|
|
2843
|
+
voiceName: tokenVoiceName
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
index = end + 1;
|
|
2847
|
+
}
|
|
2848
|
+
return tokens;
|
|
2849
|
+
}
|
|
2850
|
+
function location(source, offset) {
|
|
2851
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
2852
|
+
const line = before.split("\n").length;
|
|
2853
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
2854
|
+
}
|
|
2855
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
|
|
2856
|
+
diagnostics.push({
|
|
2857
|
+
...location(source, offset),
|
|
2858
|
+
message,
|
|
2859
|
+
severity,
|
|
2860
|
+
source: "ssml-static-validator",
|
|
2861
|
+
...code2 ? { code: code2 } : {}
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
function isSupportedProsodyRate(value) {
|
|
2865
|
+
const trimmed = value.trim();
|
|
2866
|
+
if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
|
|
2867
|
+
const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
|
|
2868
|
+
if (!multiplier) return false;
|
|
2869
|
+
const numericValue = Number(multiplier[1]);
|
|
2870
|
+
return numericValue >= 0.5 && numericValue <= 2;
|
|
2871
|
+
}
|
|
2872
|
+
function isValidAzureAudioDuration(value) {
|
|
2873
|
+
const trimmed = value.trim();
|
|
2874
|
+
const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
|
|
2875
|
+
if (numeric) return Number(numeric[1]) > 0;
|
|
2876
|
+
const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
|
|
2877
|
+
if (!clock) return false;
|
|
2878
|
+
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
2879
|
+
}
|
|
2880
|
+
function isValidAzureBackgroundAudioDuration(value) {
|
|
2881
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
|
|
2882
|
+
if (!match) return false;
|
|
2883
|
+
const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
|
|
2884
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
|
|
2885
|
+
}
|
|
2886
|
+
function attr(token, name) {
|
|
2887
|
+
return token.attributes.get(name.toLowerCase());
|
|
2888
|
+
}
|
|
2889
|
+
var DEFAULT_LANGUAGE_ALIASES = {
|
|
2890
|
+
"zh-CN": ["zh-Hans"],
|
|
2891
|
+
"zh-TW": ["zh-Hant"]
|
|
2892
|
+
};
|
|
2893
|
+
function canonicalLanguageTag(language) {
|
|
2894
|
+
const trimmed = language.trim();
|
|
2895
|
+
if (!trimmed) return "";
|
|
2896
|
+
try {
|
|
2897
|
+
return new Intl.Locale(trimmed).toString().toLowerCase();
|
|
2898
|
+
} catch {
|
|
2899
|
+
return trimmed.toLowerCase();
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
function createLanguageNormalizer(options) {
|
|
2903
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2904
|
+
const addAliasGroup = (canonical, values) => {
|
|
2905
|
+
const normalizedCanonical = canonicalLanguageTag(canonical);
|
|
2906
|
+
if (!normalizedCanonical) return;
|
|
2907
|
+
aliases.set(normalizedCanonical, normalizedCanonical);
|
|
2908
|
+
for (const value of values) {
|
|
2909
|
+
const normalizedValue = canonicalLanguageTag(value);
|
|
2910
|
+
if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
|
|
2914
|
+
for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
|
|
2915
|
+
addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
|
|
2916
|
+
return (language) => {
|
|
2917
|
+
const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
|
|
2918
|
+
const normalized = canonicalLanguageTag(customValue);
|
|
2919
|
+
return aliases.get(normalized) ?? normalized;
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
function voiceLocalePrefix(voiceName) {
|
|
2923
|
+
const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
|
|
2924
|
+
if (!match?.groups) return void 0;
|
|
2925
|
+
const tag = `${match.groups.language}-${match.groups.region}`;
|
|
2926
|
+
return {
|
|
2927
|
+
language: match.groups.language.toLowerCase(),
|
|
2928
|
+
region: match.groups.region.toLowerCase(),
|
|
2929
|
+
tag
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
function definitionFromStyleMap(voiceName, styles) {
|
|
2933
|
+
return {
|
|
2934
|
+
name: voiceName,
|
|
2935
|
+
locale: voiceLocalePrefix(voiceName)?.tag ?? "",
|
|
2936
|
+
styles
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
function normalizeVoiceCatalog(options) {
|
|
2940
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2941
|
+
for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
|
|
2942
|
+
for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2943
|
+
for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2944
|
+
for (const definition of options.customVoiceDefinitions ?? [])
|
|
2945
|
+
definitions.set(definition.name.toLowerCase(), definition);
|
|
2946
|
+
for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
|
|
2947
|
+
const key = voiceName.toLowerCase();
|
|
2948
|
+
const current = definitions.get(key);
|
|
2949
|
+
definitions.set(key, {
|
|
2950
|
+
...current ?? definitionFromStyleMap(voiceName, styles),
|
|
2951
|
+
name: current?.name ?? voiceName,
|
|
2952
|
+
styles: styles.map((style) => style.toLowerCase())
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
return definitions;
|
|
2956
|
+
}
|
|
2957
|
+
function diagnosticSeverity(policy) {
|
|
2958
|
+
if (policy === "ignore") return void 0;
|
|
2959
|
+
return policy === "error" ? "error" : "warning";
|
|
2960
|
+
}
|
|
2961
|
+
function languagePart(language) {
|
|
2962
|
+
try {
|
|
2963
|
+
return new Intl.Locale(language).language.toLowerCase();
|
|
2964
|
+
} catch {
|
|
2965
|
+
return language.split("-")[0]?.toLowerCase() ?? "";
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
|
|
2969
|
+
const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
|
|
2970
|
+
if (candidateLanguages.length === 0 || !language.trim()) return void 0;
|
|
2971
|
+
const normalizedLanguage = normalizeLanguage(language);
|
|
2972
|
+
const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
|
|
2973
|
+
if (normalizedCandidates.includes(normalizedLanguage)) return true;
|
|
2974
|
+
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
2975
|
+
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
2976
|
+
}
|
|
2977
|
+
function canonicalTagName(name) {
|
|
2978
|
+
const normalized = name.toLowerCase();
|
|
2979
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
2980
|
+
if (normalized === "sayas") return "say-as";
|
|
2981
|
+
return normalized;
|
|
2982
|
+
}
|
|
2983
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
2984
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
2985
|
+
return;
|
|
2986
|
+
const tagName = canonicalTagName(token.name);
|
|
2987
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
2988
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
2989
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
2990
|
+
addDiagnostic(
|
|
2991
|
+
diagnostics,
|
|
2992
|
+
source,
|
|
2993
|
+
token.start,
|
|
2994
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
2995
|
+
"error",
|
|
2996
|
+
"azure-unsupported-tag-for-voice"
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
3001
|
+
const src = attr(token, "src");
|
|
3002
|
+
if (!src) {
|
|
3003
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
let parsed;
|
|
3007
|
+
try {
|
|
3008
|
+
parsed = new URL(src);
|
|
3009
|
+
} catch {
|
|
3010
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
3014
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
3015
|
+
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
3016
|
+
try {
|
|
3017
|
+
return new URL(allowedOrigin).origin === parsed.origin;
|
|
3018
|
+
} catch {
|
|
3019
|
+
return allowedOrigin === parsed.origin;
|
|
3020
|
+
}
|
|
3021
|
+
}) ?? false;
|
|
3022
|
+
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
3023
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
3024
|
+
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
3025
|
+
addDiagnostic(
|
|
3026
|
+
diagnostics,
|
|
3027
|
+
source,
|
|
3028
|
+
token.start,
|
|
3029
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
3033
|
+
const name = token.name.toLowerCase();
|
|
3034
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
3035
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
3036
|
+
if (name === "break") {
|
|
3037
|
+
const time = attr(token, "time");
|
|
3038
|
+
const strength = attr(token, "strength");
|
|
3039
|
+
if (!time && !strength)
|
|
3040
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
3041
|
+
if (time && strength)
|
|
3042
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
3043
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
3044
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
3045
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
3046
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
3047
|
+
}
|
|
3048
|
+
if (name === "prosody") {
|
|
3049
|
+
const rate = attr(token, "rate");
|
|
3050
|
+
const pitch = attr(token, "pitch");
|
|
3051
|
+
const volume = attr(token, "volume");
|
|
3052
|
+
if (rate && !isSupportedProsodyRate(rate))
|
|
3053
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
3054
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
3055
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
3056
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
3057
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
3058
|
+
}
|
|
3059
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
3060
|
+
const style = attr(token, "style");
|
|
3061
|
+
if (!style?.trim())
|
|
3062
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
3063
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
3064
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
3065
|
+
addDiagnostic(
|
|
3066
|
+
diagnostics,
|
|
3067
|
+
source,
|
|
3068
|
+
token.start,
|
|
3069
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
3070
|
+
);
|
|
3071
|
+
const role = attr(token, "role");
|
|
3072
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
3073
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
3074
|
+
const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
|
|
3075
|
+
const supportedStyles = definition?.styles;
|
|
3076
|
+
const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
|
|
3077
|
+
if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
|
|
3078
|
+
addDiagnostic(
|
|
3079
|
+
diagnostics,
|
|
3080
|
+
source,
|
|
3081
|
+
token.start,
|
|
3082
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
3083
|
+
severity,
|
|
3084
|
+
"azure-unsupported-style"
|
|
3085
|
+
);
|
|
3086
|
+
if (style && voiceName && !definition && severity)
|
|
3087
|
+
addDiagnostic(
|
|
3088
|
+
diagnostics,
|
|
3089
|
+
source,
|
|
3090
|
+
token.start,
|
|
3091
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
3092
|
+
severity
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
if (name === "say-as" || name === "sayas") {
|
|
3096
|
+
const interpretAs = attr(token, "interpret-as");
|
|
3097
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
3098
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
3099
|
+
}
|
|
3100
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
3101
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
3102
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
3103
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
3104
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
3105
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
3106
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
3107
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
3108
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
3109
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
3110
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
3111
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
3112
|
+
if (name === "lexicon") {
|
|
3113
|
+
const uri = attr(token, "uri");
|
|
3114
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
3115
|
+
else {
|
|
3116
|
+
try {
|
|
3117
|
+
const parsed = new URL(uri);
|
|
3118
|
+
if (parsed.protocol !== "https:")
|
|
3119
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
3120
|
+
} catch {
|
|
3121
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (name === "mstts:silence") {
|
|
3126
|
+
const type = attr(token, "type");
|
|
3127
|
+
const value = attr(token, "value");
|
|
3128
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
3129
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
3130
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
3131
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
3132
|
+
}
|
|
3133
|
+
if (name === "mstts:audioduration") {
|
|
3134
|
+
const value = attr(token, "value");
|
|
3135
|
+
if (!value || !isValidAzureAudioDuration(value))
|
|
3136
|
+
addDiagnostic(
|
|
3137
|
+
diagnostics,
|
|
3138
|
+
source,
|
|
3139
|
+
token.start,
|
|
3140
|
+
'<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
|
|
3141
|
+
);
|
|
3142
|
+
if (!token.selfClosing)
|
|
3143
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
|
|
3144
|
+
}
|
|
3145
|
+
if (name === "mstts:viseme") {
|
|
3146
|
+
const type = attr(token, "type");
|
|
3147
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
3148
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
3149
|
+
}
|
|
3150
|
+
if (name === "audio") {
|
|
3151
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
3152
|
+
}
|
|
3153
|
+
if (name === "mstts:turn") {
|
|
3154
|
+
if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
|
|
3155
|
+
addDiagnostic(
|
|
3156
|
+
diagnostics,
|
|
3157
|
+
source,
|
|
3158
|
+
token.start,
|
|
3159
|
+
'<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
|
|
3160
|
+
);
|
|
3161
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
3162
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
3163
|
+
}
|
|
3164
|
+
if (name === "mstts:backgroundaudio") {
|
|
3165
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
3166
|
+
const volume = attr(token, "volume");
|
|
3167
|
+
if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
|
|
3168
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
3169
|
+
for (const [attribute, value] of [
|
|
3170
|
+
["fadein", attr(token, "fadein")],
|
|
3171
|
+
["fadeout", attr(token, "fadeout")]
|
|
3172
|
+
]) {
|
|
3173
|
+
if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
|
|
3174
|
+
addDiagnostic(
|
|
3175
|
+
diagnostics,
|
|
3176
|
+
source,
|
|
3177
|
+
token.start,
|
|
3178
|
+
`<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
|
|
3182
|
+
addDiagnostic(
|
|
3183
|
+
diagnostics,
|
|
3184
|
+
source,
|
|
3185
|
+
token.start,
|
|
3186
|
+
"<mstts:backgroundaudio> must be the first element directly under <speak>."
|
|
3187
|
+
);
|
|
3188
|
+
if (!token.selfClosing)
|
|
3189
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
3193
|
+
const diagnostics = [];
|
|
3194
|
+
if (typeof ssml !== "string") {
|
|
3195
|
+
return [
|
|
3196
|
+
{
|
|
3197
|
+
line: 1,
|
|
3198
|
+
column: 1,
|
|
3199
|
+
message: "SSML input must be a string",
|
|
3200
|
+
severity: "error",
|
|
3201
|
+
source: "ssml-static-validator"
|
|
3202
|
+
}
|
|
3203
|
+
];
|
|
3204
|
+
}
|
|
3205
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
3206
|
+
if (ssml.length > maxLength)
|
|
3207
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
3208
|
+
try {
|
|
3209
|
+
parseSsml(ssml);
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
3212
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
3213
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
3214
|
+
return diagnostics;
|
|
3215
|
+
}
|
|
3216
|
+
const tokens = tokenizeElements(ssml);
|
|
3217
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
3218
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
3219
|
+
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
3220
|
+
for (const [index, token] of backgroundAudioTokens.entries()) {
|
|
3221
|
+
if (index > 0)
|
|
3222
|
+
addDiagnostic(
|
|
3223
|
+
diagnostics,
|
|
3224
|
+
ssml,
|
|
3225
|
+
token.start,
|
|
3226
|
+
"An SSML document can contain at most one <mstts:backgroundaudio> element."
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
if (!speak || voices.length === 0)
|
|
3230
|
+
addDiagnostic(
|
|
3231
|
+
diagnostics,
|
|
3232
|
+
ssml,
|
|
3233
|
+
speak?.start ?? 0,
|
|
3234
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
3235
|
+
);
|
|
3236
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
3237
|
+
const voiceCatalog = normalizeVoiceCatalog(options);
|
|
3238
|
+
const normalizeLanguage = createLanguageNormalizer(options);
|
|
3239
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
3240
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
3241
|
+
for (const token of voicesToValidate) {
|
|
3242
|
+
const name = attr(token, "name")?.trim();
|
|
3243
|
+
const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
|
|
3244
|
+
const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
|
|
3245
|
+
if (name && !definition && policySeverity)
|
|
3246
|
+
addDiagnostic(
|
|
3247
|
+
diagnostics,
|
|
3248
|
+
ssml,
|
|
3249
|
+
token.start,
|
|
3250
|
+
`Unknown voice "${name}" is not registered in the voice catalog.`,
|
|
3251
|
+
policySeverity,
|
|
3252
|
+
"azure-unknown-voice"
|
|
3253
|
+
);
|
|
3254
|
+
if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
|
|
3255
|
+
addDiagnostic(
|
|
3256
|
+
diagnostics,
|
|
3257
|
+
ssml,
|
|
3258
|
+
token.start,
|
|
3259
|
+
`Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
|
|
3260
|
+
"warning",
|
|
3261
|
+
"azure-locale-mismatch"
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
for (const token of tokens) {
|
|
3265
|
+
const tokenName = token.name.toLowerCase();
|
|
3266
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
3267
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
3268
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
3269
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
3270
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
3271
|
+
addDiagnostic(
|
|
3272
|
+
diagnostics,
|
|
3273
|
+
ssml,
|
|
3274
|
+
token.start,
|
|
3275
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
3276
|
+
"error",
|
|
3277
|
+
"azure-unsupported-model-for-voice"
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
return diagnostics;
|
|
3282
|
+
}
|
|
3283
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
3284
|
+
apiVersion: "2025-10-01",
|
|
3285
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
3286
|
+
regions: [],
|
|
3287
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
3288
|
+
};
|
|
2511
3289
|
|
|
2512
3290
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
2513
3291
|
function getDocumentChildren(document2) {
|
|
@@ -3266,7 +4044,7 @@ function createSsmlInsertionEdit(source, startOffset, endOffset, template, eol =
|
|
|
3266
4044
|
}
|
|
3267
4045
|
|
|
3268
4046
|
// packages/ssml-editor-react/src/ssmlContext.ts
|
|
3269
|
-
function
|
|
4047
|
+
function findTagEnd3(source, start, limit) {
|
|
3270
4048
|
let quote;
|
|
3271
4049
|
for (let index = start + 1; index < limit; index += 1) {
|
|
3272
4050
|
const character = source[index];
|
|
@@ -3336,7 +4114,7 @@ function findActiveSsmlTags(source, offset) {
|
|
|
3336
4114
|
index = end;
|
|
3337
4115
|
continue;
|
|
3338
4116
|
}
|
|
3339
|
-
const tagEnd =
|
|
4117
|
+
const tagEnd = findTagEnd3(source, tagStart, source.length);
|
|
3340
4118
|
const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
|
|
3341
4119
|
const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
3342
4120
|
const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
@@ -3379,7 +4157,7 @@ function findSsmlVoiceContext(source, offset) {
|
|
|
3379
4157
|
index = end === -1 || end + 2 > limit ? limit : end + 2;
|
|
3380
4158
|
continue;
|
|
3381
4159
|
}
|
|
3382
|
-
const tagEnd =
|
|
4160
|
+
const tagEnd = findTagEnd3(source, tagStart, limit);
|
|
3383
4161
|
if (tagEnd === -1) {
|
|
3384
4162
|
break;
|
|
3385
4163
|
}
|
|
@@ -4139,6 +4917,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
4139
4917
|
label: "mstts:audioduration",
|
|
4140
4918
|
insertText: '<mstts:audioduration value="10s" />'
|
|
4141
4919
|
},
|
|
4920
|
+
{
|
|
4921
|
+
label: "mstts:dialog",
|
|
4922
|
+
insertText: `<mstts:dialog>
|
|
4923
|
+
<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
|
|
4924
|
+
</mstts:dialog>`
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
label: "mstts:turn",
|
|
4928
|
+
insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
|
|
4929
|
+
},
|
|
4930
|
+
{
|
|
4931
|
+
label: "mstts:backgroundaudio",
|
|
4932
|
+
insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
|
|
4933
|
+
},
|
|
4934
|
+
{
|
|
4935
|
+
label: "mstts:ttsembedding",
|
|
4936
|
+
insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
|
|
4937
|
+
},
|
|
4142
4938
|
{
|
|
4143
4939
|
label: "sub",
|
|
4144
4940
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -4317,7 +5113,7 @@ function registerSsmlCodeActions(monaco) {
|
|
|
4317
5113
|
|
|
4318
5114
|
// packages/ssml-editor-react/src/ssmlCodeLens.ts
|
|
4319
5115
|
var CODE_LENS_COMMAND = "ssml-editor.codeLens";
|
|
4320
|
-
function
|
|
5116
|
+
function findTagEnd4(source, start) {
|
|
4321
5117
|
let quote;
|
|
4322
5118
|
for (let index = start + 1; index < source.length; index += 1) {
|
|
4323
5119
|
const character = source[index];
|
|
@@ -4357,7 +5153,7 @@ function getElementEnd(source, tagName, tagEnd) {
|
|
|
4357
5153
|
index = index === -1 ? source.length : index + 3;
|
|
4358
5154
|
continue;
|
|
4359
5155
|
}
|
|
4360
|
-
const nextEnd =
|
|
5156
|
+
const nextEnd = findTagEnd4(source, nextStart);
|
|
4361
5157
|
if (nextEnd === -1) {
|
|
4362
5158
|
break;
|
|
4363
5159
|
}
|
|
@@ -4409,7 +5205,7 @@ function registerSsmlCodeLens(monaco, editor, onOpenPopover) {
|
|
|
4409
5205
|
index = index === -1 ? source.length : index + 3;
|
|
4410
5206
|
continue;
|
|
4411
5207
|
}
|
|
4412
|
-
const tagEnd =
|
|
5208
|
+
const tagEnd = findTagEnd4(source, tagStart);
|
|
4413
5209
|
if (tagEnd === -1) {
|
|
4414
5210
|
break;
|
|
4415
5211
|
}
|
|
@@ -4806,6 +5602,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
4806
5602
|
example: "10s"
|
|
4807
5603
|
}
|
|
4808
5604
|
]
|
|
5605
|
+
},
|
|
5606
|
+
{
|
|
5607
|
+
name: "mstts:dialog",
|
|
5608
|
+
description: "Groups multiple Azure dialog turns that can use different voices.",
|
|
5609
|
+
parameters: []
|
|
5610
|
+
},
|
|
5611
|
+
{
|
|
5612
|
+
name: "mstts:turn",
|
|
5613
|
+
description: "Adds one dialog turn using the required Azure voice name.",
|
|
5614
|
+
parameters: [
|
|
5615
|
+
{
|
|
5616
|
+
name: "voice",
|
|
5617
|
+
description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
|
|
5618
|
+
example: "en-US-JennyNeural"
|
|
5619
|
+
}
|
|
5620
|
+
]
|
|
5621
|
+
},
|
|
5622
|
+
{
|
|
5623
|
+
name: "mstts:backgroundaudio",
|
|
5624
|
+
description: "Plays background audio while speech is synthesized.",
|
|
5625
|
+
parameters: [
|
|
5626
|
+
{
|
|
5627
|
+
name: "src",
|
|
5628
|
+
description: "An absolute HTTP(S) URL for the background audio file.",
|
|
5629
|
+
example: "https://example.com/music.mp3"
|
|
5630
|
+
},
|
|
5631
|
+
{
|
|
5632
|
+
name: "volume",
|
|
5633
|
+
description: "The background audio volume, for example `-3dB` or `medium`.",
|
|
5634
|
+
example: "-3dB"
|
|
5635
|
+
},
|
|
5636
|
+
{
|
|
5637
|
+
name: "fadein",
|
|
5638
|
+
description: "The fade-in duration, for example `1s`.",
|
|
5639
|
+
example: "1s"
|
|
5640
|
+
},
|
|
5641
|
+
{
|
|
5642
|
+
name: "fadeout",
|
|
5643
|
+
description: "The fade-out duration, for example `500ms`.",
|
|
5644
|
+
example: "500ms"
|
|
5645
|
+
}
|
|
5646
|
+
]
|
|
5647
|
+
},
|
|
5648
|
+
{
|
|
5649
|
+
name: "mstts:ttsembedding",
|
|
5650
|
+
description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
|
|
5651
|
+
parameters: []
|
|
5652
|
+
},
|
|
5653
|
+
{
|
|
5654
|
+
name: "mstts:embedding",
|
|
5655
|
+
description: "Specifies embedding metadata for custom voice scenarios.",
|
|
5656
|
+
parameters: []
|
|
5657
|
+
},
|
|
5658
|
+
{
|
|
5659
|
+
name: "mstts:voiceconversion",
|
|
5660
|
+
description: "Specifies voice conversion metadata for custom voice scenarios.",
|
|
5661
|
+
parameters: []
|
|
4809
5662
|
}
|
|
4810
5663
|
];
|
|
4811
5664
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -4864,7 +5717,7 @@ function toRange(source, token) {
|
|
|
4864
5717
|
function containsOffset(token, offset) {
|
|
4865
5718
|
return offset >= token.start && offset < token.end;
|
|
4866
5719
|
}
|
|
4867
|
-
function
|
|
5720
|
+
function findTagEnd5(source, start) {
|
|
4868
5721
|
let quote;
|
|
4869
5722
|
for (let index = start; index < source.length; index += 1) {
|
|
4870
5723
|
const character = source[index];
|
|
@@ -4995,7 +5848,7 @@ function findTagAtOffset(source, offset) {
|
|
|
4995
5848
|
searchStart = tokenEnd2;
|
|
4996
5849
|
continue;
|
|
4997
5850
|
}
|
|
4998
|
-
const tagEnd =
|
|
5851
|
+
const tagEnd = findTagEnd5(source, start + 1);
|
|
4999
5852
|
const contentEnd = tagEnd ?? source.length;
|
|
5000
5853
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
5001
5854
|
if (offset < tokenEnd) {
|
|
@@ -5410,6 +6263,140 @@ function useSsmlMonaco({
|
|
|
5410
6263
|
return { editorRef, onMount };
|
|
5411
6264
|
}
|
|
5412
6265
|
|
|
6266
|
+
// packages/ssml-editor-react/src/components/VisualSsmlEditor.tsx
|
|
6267
|
+
var import_react3 = require("react");
|
|
6268
|
+
function isElement(node) {
|
|
6269
|
+
return typeof node !== "string" && node.type !== "text";
|
|
6270
|
+
}
|
|
6271
|
+
function elementLabel(element) {
|
|
6272
|
+
return element.type === "custom" || element.type === "element" ? element.name : element.type;
|
|
6273
|
+
}
|
|
6274
|
+
function collectTextLeaves(nodes, path = [], ancestors = []) {
|
|
6275
|
+
return nodes.flatMap((node, index) => {
|
|
6276
|
+
const currentPath = [...path, index];
|
|
6277
|
+
if (typeof node === "string") return [{ ancestors, path: currentPath, value: node }];
|
|
6278
|
+
if (node.type === "text") return [{ ancestors, path: currentPath, value: node.value }];
|
|
6279
|
+
return collectTextLeaves(node.children ?? [], currentPath, [...ancestors, elementLabel(node)]);
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
function updateNodesAtPath(nodes, path, update) {
|
|
6283
|
+
if (path.length === 0) return nodes;
|
|
6284
|
+
const [index, ...rest] = path;
|
|
6285
|
+
return nodes.flatMap((node, nodeIndex) => {
|
|
6286
|
+
if (nodeIndex !== index) return [node];
|
|
6287
|
+
if (rest.length === 0) {
|
|
6288
|
+
const next = update(node);
|
|
6289
|
+
return Array.isArray(next) ? next : [next];
|
|
6290
|
+
}
|
|
6291
|
+
if (!isElement(node)) return [node];
|
|
6292
|
+
return [{ ...node, children: updateNodesAtPath(node.children ?? [], rest, update) }];
|
|
6293
|
+
});
|
|
6294
|
+
}
|
|
6295
|
+
function updateTextAtPath(document2, path, value) {
|
|
6296
|
+
return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
|
|
6297
|
+
}
|
|
6298
|
+
function wrapTextAtPath(document2, path, start, end, tag, attributes) {
|
|
6299
|
+
return {
|
|
6300
|
+
...document2,
|
|
6301
|
+
children: updateNodesAtPath(document2.children ?? [], path, (node) => {
|
|
6302
|
+
const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
|
|
6303
|
+
if (!value || start === end) return node;
|
|
6304
|
+
if (tag === "break") {
|
|
6305
|
+
return [
|
|
6306
|
+
value.slice(0, start),
|
|
6307
|
+
{ type: tag, attributes, children: [] },
|
|
6308
|
+
value.slice(start)
|
|
6309
|
+
].filter((part) => typeof part === "string" ? part.length > 0 : true);
|
|
6310
|
+
}
|
|
6311
|
+
const selected = value.slice(start, end);
|
|
6312
|
+
const wrapped = { type: tag, attributes, children: [selected] };
|
|
6313
|
+
return [value.slice(0, start), wrapped, value.slice(end)].filter(
|
|
6314
|
+
(part) => typeof part === "string" ? part.length > 0 : true
|
|
6315
|
+
);
|
|
6316
|
+
})
|
|
6317
|
+
};
|
|
6318
|
+
}
|
|
6319
|
+
function TreeNode({
|
|
6320
|
+
node,
|
|
6321
|
+
path,
|
|
6322
|
+
selectedPath,
|
|
6323
|
+
onSelect
|
|
6324
|
+
}) {
|
|
6325
|
+
if (!isElement(node)) return null;
|
|
6326
|
+
const name = elementLabel(node);
|
|
6327
|
+
const isSelected = selectedPath?.join(".") === path.join(".");
|
|
6328
|
+
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(
|
|
6329
|
+
TreeNode,
|
|
6330
|
+
{
|
|
6331
|
+
key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
|
|
6332
|
+
node: child,
|
|
6333
|
+
path: [...path, index],
|
|
6334
|
+
selectedPath,
|
|
6335
|
+
onSelect
|
|
6336
|
+
}
|
|
6337
|
+
))));
|
|
6338
|
+
}
|
|
6339
|
+
function VisualSsmlEditor({
|
|
6340
|
+
document: document2,
|
|
6341
|
+
readOnly = false,
|
|
6342
|
+
onChange,
|
|
6343
|
+
onPreviewSelection
|
|
6344
|
+
}) {
|
|
6345
|
+
const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
|
|
6346
|
+
const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
|
|
6347
|
+
const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
|
|
6348
|
+
const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
|
|
6349
|
+
const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
|
|
6350
|
+
const commit = (nextDocument) => onChange?.(nextDocument);
|
|
6351
|
+
const applyWrapper = (tag, attributes) => {
|
|
6352
|
+
if (readOnly || !selectedLeaf) return;
|
|
6353
|
+
const start = selection.start === selection.end ? 0 : selection.start;
|
|
6354
|
+
const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
|
|
6355
|
+
commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
|
|
6356
|
+
};
|
|
6357
|
+
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(
|
|
6358
|
+
TreeNode,
|
|
6359
|
+
{
|
|
6360
|
+
key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
|
|
6361
|
+
node,
|
|
6362
|
+
path: [index],
|
|
6363
|
+
selectedPath,
|
|
6364
|
+
onSelect: setSelectedPath
|
|
6365
|
+
}
|
|
6366
|
+
)))), /* @__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(
|
|
6367
|
+
"textarea",
|
|
6368
|
+
{
|
|
6369
|
+
value: selectedLeaf.value,
|
|
6370
|
+
readOnly,
|
|
6371
|
+
onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
|
|
6372
|
+
onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
|
|
6373
|
+
}
|
|
6374
|
+
)), /* @__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(
|
|
6375
|
+
"button",
|
|
6376
|
+
{
|
|
6377
|
+
type: "button",
|
|
6378
|
+
disabled: readOnly,
|
|
6379
|
+
onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
|
|
6380
|
+
},
|
|
6381
|
+
"Emotion"
|
|
6382
|
+
), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
|
|
6383
|
+
"button",
|
|
6384
|
+
{
|
|
6385
|
+
type: "button",
|
|
6386
|
+
disabled: readOnly,
|
|
6387
|
+
onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
|
|
6388
|
+
},
|
|
6389
|
+
"Pronunciation"
|
|
6390
|
+
), onPreviewSelection && /* @__PURE__ */ React.createElement(
|
|
6391
|
+
"button",
|
|
6392
|
+
{
|
|
6393
|
+
type: "button",
|
|
6394
|
+
onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
|
|
6395
|
+
},
|
|
6396
|
+
"Preview selection"
|
|
6397
|
+
))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));
|
|
6398
|
+
}
|
|
6399
|
+
|
|
5413
6400
|
// packages/ssml-editor-react/src/SsmlEditor.tsx
|
|
5414
6401
|
var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
|
|
5415
6402
|
var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
|
|
@@ -5876,6 +6863,60 @@ var STYLE_CSS = `
|
|
|
5876
6863
|
> .ssml-editor-help-settings-summary::before {
|
|
5877
6864
|
content: "\u25BE";
|
|
5878
6865
|
}
|
|
6866
|
+
[data-ssml-editor] .ssml-editor-visual-layout {
|
|
6867
|
+
display: grid;
|
|
6868
|
+
grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
|
|
6869
|
+
gap: 1rem;
|
|
6870
|
+
}
|
|
6871
|
+
[data-ssml-editor] .ssml-editor-visual-tree,
|
|
6872
|
+
[data-ssml-editor] .ssml-editor-visual-form {
|
|
6873
|
+
display: grid;
|
|
6874
|
+
gap: 0.5rem;
|
|
6875
|
+
align-content: start;
|
|
6876
|
+
}
|
|
6877
|
+
[data-ssml-editor] .ssml-editor-visual-tree ul {
|
|
6878
|
+
margin: 0;
|
|
6879
|
+
padding-left: 1.25rem;
|
|
6880
|
+
}
|
|
6881
|
+
[data-ssml-editor] .ssml-editor-visual-tree button,
|
|
6882
|
+
[data-ssml-editor] .ssml-editor-visual-breadcrumb button,
|
|
6883
|
+
[data-ssml-editor] .ssml-editor-visual-actions button {
|
|
6884
|
+
padding: 0.35rem 0.5rem;
|
|
6885
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
6886
|
+
border-radius: 0.25rem;
|
|
6887
|
+
color: var(--ssml-editor-color);
|
|
6888
|
+
background: var(--ssml-editor-control-bg);
|
|
6889
|
+
cursor: pointer;
|
|
6890
|
+
}
|
|
6891
|
+
[data-ssml-editor] .ssml-editor-visual-tree button[aria-current] {
|
|
6892
|
+
border-color: var(--ssml-editor-active-border);
|
|
6893
|
+
background: var(--ssml-editor-active-bg);
|
|
6894
|
+
}
|
|
6895
|
+
[data-ssml-editor] .ssml-editor-visual-breadcrumb,
|
|
6896
|
+
[data-ssml-editor] .ssml-editor-visual-actions {
|
|
6897
|
+
display: flex;
|
|
6898
|
+
flex-wrap: wrap;
|
|
6899
|
+
gap: 0.4rem;
|
|
6900
|
+
align-items: center;
|
|
6901
|
+
}
|
|
6902
|
+
[data-ssml-editor] .ssml-editor-visual-form textarea {
|
|
6903
|
+
box-sizing: border-box;
|
|
6904
|
+
width: 100%;
|
|
6905
|
+
min-height: 6rem;
|
|
6906
|
+
padding: 0.5rem;
|
|
6907
|
+
color: var(--ssml-editor-color);
|
|
6908
|
+
background: var(--ssml-editor-control-bg);
|
|
6909
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
6910
|
+
border-radius: 0.25rem;
|
|
6911
|
+
font: inherit;
|
|
6912
|
+
}
|
|
6913
|
+
[data-ssml-editor] .ssml-editor-visual-errors {
|
|
6914
|
+
padding: 0.5rem;
|
|
6915
|
+
color: var(--ssml-editor-error);
|
|
6916
|
+
background: var(--ssml-editor-error-bg);
|
|
6917
|
+
border: 1px solid var(--ssml-editor-error);
|
|
6918
|
+
border-radius: 0.25rem;
|
|
6919
|
+
}
|
|
5879
6920
|
`.trim();
|
|
5880
6921
|
function injectEditorTheme() {
|
|
5881
6922
|
if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
|
|
@@ -5907,7 +6948,7 @@ function ToolbarDecorationsSwitch({
|
|
|
5907
6948
|
/* @__PURE__ */ React.createElement("span", { className: "ssml-editor-switch-thumb", style: editorStyles.toolbarSwitchThumb, "aria-hidden": "true" })
|
|
5908
6949
|
));
|
|
5909
6950
|
}
|
|
5910
|
-
var SsmlEditor = (0,
|
|
6951
|
+
var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
|
|
5911
6952
|
document: document2,
|
|
5912
6953
|
onChange,
|
|
5913
6954
|
onSsmlChange,
|
|
@@ -5945,8 +6986,11 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
5945
6986
|
toolbarStyle,
|
|
5946
6987
|
displayClassName,
|
|
5947
6988
|
displayStyle,
|
|
5948
|
-
loadingFallback
|
|
6989
|
+
loadingFallback,
|
|
6990
|
+
editMode: editModeProp = "code"
|
|
5949
6991
|
}, ref) {
|
|
6992
|
+
const [editMode, setEditMode] = (0, import_react4.useState)(editModeProp);
|
|
6993
|
+
(0, import_react4.useEffect)(() => setEditMode(editModeProp), [editModeProp]);
|
|
5950
6994
|
const resolvedEditorOptions = {
|
|
5951
6995
|
...settings,
|
|
5952
6996
|
...editorOptions,
|
|
@@ -6024,6 +7068,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6024
7068
|
);
|
|
6025
7069
|
const {
|
|
6026
7070
|
editorRef,
|
|
7071
|
+
draftDocument,
|
|
6027
7072
|
text,
|
|
6028
7073
|
selectionOverlay,
|
|
6029
7074
|
activeTags,
|
|
@@ -6035,6 +7080,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6035
7080
|
syntaxError,
|
|
6036
7081
|
setSyntaxError,
|
|
6037
7082
|
helpPanelId,
|
|
7083
|
+
commit,
|
|
6038
7084
|
handleTextChange,
|
|
6039
7085
|
handleInsert,
|
|
6040
7086
|
handleInsertBreak,
|
|
@@ -6082,7 +7128,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6082
7128
|
enableCodeLens,
|
|
6083
7129
|
onOpenPopover: handleCodeLensAction
|
|
6084
7130
|
});
|
|
6085
|
-
(0,
|
|
7131
|
+
(0, import_react4.useImperativeHandle)(
|
|
6086
7132
|
ref,
|
|
6087
7133
|
() => ({
|
|
6088
7134
|
getSelectedSsml: getSelectedSsml2,
|
|
@@ -6265,7 +7311,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6265
7311
|
previousRenderedGroupId = groupId;
|
|
6266
7312
|
hasRenderedToolbarItem = true;
|
|
6267
7313
|
renderedItems.push(
|
|
6268
|
-
/* @__PURE__ */ React.createElement(
|
|
7314
|
+
/* @__PURE__ */ React.createElement(import_react4.Fragment, { key: id }, showSeparator && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }), render())
|
|
6269
7315
|
);
|
|
6270
7316
|
}
|
|
6271
7317
|
return renderedItems;
|
|
@@ -6294,11 +7340,40 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6294
7340
|
"aria-label": copy.toolbarAriaLabel,
|
|
6295
7341
|
"data-ssml-editor-toolbar-actions": ""
|
|
6296
7342
|
},
|
|
6297
|
-
renderToolbarItems()
|
|
7343
|
+
renderToolbarItems(),
|
|
7344
|
+
/* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarSeparator, "aria-hidden": "true" }),
|
|
7345
|
+
/* @__PURE__ */ React.createElement(
|
|
7346
|
+
"button",
|
|
7347
|
+
{
|
|
7348
|
+
type: "button",
|
|
7349
|
+
style: editMode === "visual" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
|
|
7350
|
+
"aria-pressed": editMode === "visual",
|
|
7351
|
+
onClick: () => setEditMode("visual")
|
|
7352
|
+
},
|
|
7353
|
+
"Visual"
|
|
7354
|
+
),
|
|
7355
|
+
/* @__PURE__ */ React.createElement(
|
|
7356
|
+
"button",
|
|
7357
|
+
{
|
|
7358
|
+
type: "button",
|
|
7359
|
+
style: editMode === "code" ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle,
|
|
7360
|
+
"aria-pressed": editMode === "code",
|
|
7361
|
+
onClick: () => setEditMode("code")
|
|
7362
|
+
},
|
|
7363
|
+
"Code"
|
|
7364
|
+
)
|
|
6298
7365
|
)
|
|
6299
7366
|
),
|
|
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
|
-
|
|
7367
|
+
/* @__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(
|
|
7368
|
+
VisualSsmlEditor,
|
|
7369
|
+
{
|
|
7370
|
+
document: draftDocument,
|
|
7371
|
+
readOnly: isReadOnly,
|
|
7372
|
+
onChange: commit,
|
|
7373
|
+
onPreviewSelection
|
|
7374
|
+
}
|
|
7375
|
+
)) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
|
|
7376
|
+
import_react5.default,
|
|
6302
7377
|
{
|
|
6303
7378
|
height: editorHeight,
|
|
6304
7379
|
language: "xml",
|
|
@@ -6359,6 +7434,7 @@ var SsmlEditor = (0, import_react3.forwardRef)(function SsmlEditor2({
|
|
|
6359
7434
|
SSML_HOVER_COPY,
|
|
6360
7435
|
SSML_INSERTIONS,
|
|
6361
7436
|
SsmlEditor,
|
|
7437
|
+
VisualSsmlEditor,
|
|
6362
7438
|
createSsmlEditorInsertionDefinition,
|
|
6363
7439
|
registerSsmlCodeLens,
|
|
6364
7440
|
updateTagAttribute
|