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/elements.js
CHANGED
|
@@ -75,7 +75,13 @@ var SSML_TAGS = {
|
|
|
75
75
|
SILENCE: "silence",
|
|
76
76
|
MSTTS_VISEME: "mstts:viseme",
|
|
77
77
|
VISEME: "viseme",
|
|
78
|
-
MSTTS_AUDIO_DURATION: "mstts:audioduration"
|
|
78
|
+
MSTTS_AUDIO_DURATION: "mstts:audioduration",
|
|
79
|
+
MSTTS_DIALOG: "mstts:dialog",
|
|
80
|
+
MSTTS_TURN: "mstts:turn",
|
|
81
|
+
MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
|
|
82
|
+
MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
|
|
83
|
+
MSTTS_EMBEDDING: "mstts:embedding",
|
|
84
|
+
MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
|
|
79
85
|
};
|
|
80
86
|
var SSML_ATTRS = {
|
|
81
87
|
VERSION: "version",
|
|
@@ -84,6 +90,8 @@ var SSML_ATTRS = {
|
|
|
84
90
|
LANG: "lang",
|
|
85
91
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
86
92
|
NAME: "name",
|
|
93
|
+
VOICE: "voice",
|
|
94
|
+
SPEAKER: "speaker",
|
|
87
95
|
EFFECT: "effect",
|
|
88
96
|
RATE: "rate",
|
|
89
97
|
PITCH: "pitch",
|
|
@@ -114,8 +122,15 @@ var SSML_ATTRS = {
|
|
|
114
122
|
ALIAS: "alias",
|
|
115
123
|
MARK: "mark",
|
|
116
124
|
URI: "uri",
|
|
125
|
+
ID: "id",
|
|
126
|
+
MODEL: "model",
|
|
127
|
+
PROFILE: "profile",
|
|
128
|
+
URL: "url",
|
|
129
|
+
SPEAKER_PROFILE_ID: "speakerProfileId",
|
|
117
130
|
TYPE: "type",
|
|
118
|
-
VALUE: "value"
|
|
131
|
+
VALUE: "value",
|
|
132
|
+
FADE_IN: "fadein",
|
|
133
|
+
FADE_OUT: "fadeout"
|
|
119
134
|
};
|
|
120
135
|
function escapeText(value) {
|
|
121
136
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -205,6 +220,30 @@ function getAttributes(element) {
|
|
|
205
220
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
206
221
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
207
222
|
break;
|
|
223
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
224
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
225
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
|
|
226
|
+
break;
|
|
227
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
228
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
229
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
230
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
231
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
232
|
+
break;
|
|
233
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
234
|
+
break;
|
|
235
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
236
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
237
|
+
break;
|
|
238
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
239
|
+
addAttribute(attributes, SSML_ATTRS.ID, element.id);
|
|
240
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
241
|
+
break;
|
|
242
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
243
|
+
addAttribute(attributes, SSML_ATTRS.URL, element.url);
|
|
244
|
+
addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
|
|
245
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
246
|
+
break;
|
|
208
247
|
case SSML_TAGS.PARAGRAPH:
|
|
209
248
|
case SSML_TAGS.SENTENCE:
|
|
210
249
|
case SSML_TAGS.WORD:
|
|
@@ -769,6 +808,54 @@ function convertElement(node) {
|
|
|
769
808
|
if (value !== void 0) element.value = value;
|
|
770
809
|
return finishElement(element, node, attributes);
|
|
771
810
|
}
|
|
811
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
812
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
813
|
+
return finishElement(element, node, attributes);
|
|
814
|
+
}
|
|
815
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
816
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
817
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
818
|
+
const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
|
|
819
|
+
if (voice !== void 0) element.voice = voice;
|
|
820
|
+
if (speaker !== void 0) element.speaker = speaker;
|
|
821
|
+
return finishElement(element, node, attributes);
|
|
822
|
+
}
|
|
823
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
824
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
825
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
826
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
827
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
828
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
829
|
+
if (src !== void 0) element.src = src;
|
|
830
|
+
if (volume !== void 0) element.volume = volume;
|
|
831
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
832
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
833
|
+
return finishElement(element, node, attributes);
|
|
834
|
+
}
|
|
835
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
836
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
837
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
838
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
839
|
+
return finishElement(element, node, attributes);
|
|
840
|
+
}
|
|
841
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
842
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
843
|
+
const id = readAttribute(attributes, SSML_ATTRS.ID);
|
|
844
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
845
|
+
if (id !== void 0) element.id = id;
|
|
846
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
847
|
+
return finishElement(element, node, attributes);
|
|
848
|
+
}
|
|
849
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
850
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
851
|
+
const url = readAttribute(attributes, SSML_ATTRS.URL);
|
|
852
|
+
const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
|
|
853
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
854
|
+
if (url !== void 0) element.url = url;
|
|
855
|
+
if (profile !== void 0) element.profile = profile;
|
|
856
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
857
|
+
return finishElement(element, node, attributes);
|
|
858
|
+
}
|
|
772
859
|
default: {
|
|
773
860
|
const element = {
|
|
774
861
|
name: node.name,
|
|
@@ -810,6 +897,683 @@ function parseSsml(xmlString) {
|
|
|
810
897
|
}
|
|
811
898
|
return document2;
|
|
812
899
|
}
|
|
900
|
+
var AZURE_VOICE_DEFINITIONS = [
|
|
901
|
+
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
902
|
+
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
903
|
+
{ name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
|
|
904
|
+
{
|
|
905
|
+
name: "en-US-GuyNeural",
|
|
906
|
+
locale: "en-US",
|
|
907
|
+
styles: [
|
|
908
|
+
"angry",
|
|
909
|
+
"cheerful",
|
|
910
|
+
"excited",
|
|
911
|
+
"friendly",
|
|
912
|
+
"hopeful",
|
|
913
|
+
"newscast",
|
|
914
|
+
"sad",
|
|
915
|
+
"shouting",
|
|
916
|
+
"terrified",
|
|
917
|
+
"unfriendly",
|
|
918
|
+
"whispering"
|
|
919
|
+
]
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
name: "en-US-JennyMultilingualNeural",
|
|
923
|
+
locale: "en-US",
|
|
924
|
+
styles: [
|
|
925
|
+
"cheerful",
|
|
926
|
+
"empathetic",
|
|
927
|
+
"excited",
|
|
928
|
+
"friendly",
|
|
929
|
+
"hopeful",
|
|
930
|
+
"sad",
|
|
931
|
+
"shouting",
|
|
932
|
+
"terrified",
|
|
933
|
+
"unfriendly",
|
|
934
|
+
"whispering"
|
|
935
|
+
]
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "en-US-JennyNeural",
|
|
939
|
+
locale: "en-US",
|
|
940
|
+
styles: [
|
|
941
|
+
"assistant",
|
|
942
|
+
"chat",
|
|
943
|
+
"customerservice",
|
|
944
|
+
"newscast",
|
|
945
|
+
"cheerful",
|
|
946
|
+
"empathetic",
|
|
947
|
+
"excited",
|
|
948
|
+
"friendly",
|
|
949
|
+
"hopeful",
|
|
950
|
+
"sad",
|
|
951
|
+
"shouting",
|
|
952
|
+
"terrified",
|
|
953
|
+
"unfriendly",
|
|
954
|
+
"whispering"
|
|
955
|
+
]
|
|
956
|
+
},
|
|
957
|
+
{ name: "es-ES-ElviraNeural", locale: "es-ES" },
|
|
958
|
+
{ name: "fil-PH-AngeloNeural", locale: "fil-PH" },
|
|
959
|
+
{ name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
960
|
+
{ name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
961
|
+
{ name: "id-ID-GadisNeural", locale: "id-ID" },
|
|
962
|
+
{ name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
|
|
963
|
+
{ name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
|
|
964
|
+
{ name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
|
|
965
|
+
{ name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
|
|
966
|
+
{ name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
|
|
967
|
+
{ name: "ms-MY-YasminNeural", locale: "ms-MY" },
|
|
968
|
+
{ name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
|
|
969
|
+
{
|
|
970
|
+
name: "ru-RU-SvetlanaNeural",
|
|
971
|
+
locale: "ru-RU",
|
|
972
|
+
styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
973
|
+
},
|
|
974
|
+
{ name: "th-TH-PremwadeeNeural", locale: "th-TH" },
|
|
975
|
+
{ name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
|
|
976
|
+
{
|
|
977
|
+
name: "zh-CN-XiaoxiaoNeural",
|
|
978
|
+
locale: "zh-CN",
|
|
979
|
+
styles: [
|
|
980
|
+
"assistant",
|
|
981
|
+
"chat",
|
|
982
|
+
"customerservice",
|
|
983
|
+
"newscast",
|
|
984
|
+
"cheerful",
|
|
985
|
+
"empathetic",
|
|
986
|
+
"excited",
|
|
987
|
+
"friendly",
|
|
988
|
+
"hopeful",
|
|
989
|
+
"sad",
|
|
990
|
+
"terrified",
|
|
991
|
+
"whispering",
|
|
992
|
+
"poetry-reading",
|
|
993
|
+
"sports_commentary",
|
|
994
|
+
"sports_commentary_excited",
|
|
995
|
+
"story"
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "zh-CN-YunxiNeural",
|
|
1000
|
+
locale: "zh-CN",
|
|
1001
|
+
styles: [
|
|
1002
|
+
"narration-relaxed",
|
|
1003
|
+
"embarrassed",
|
|
1004
|
+
"fearful",
|
|
1005
|
+
"sad",
|
|
1006
|
+
"disgruntled",
|
|
1007
|
+
"serious",
|
|
1008
|
+
"angry",
|
|
1009
|
+
"depressed",
|
|
1010
|
+
"chat",
|
|
1011
|
+
"cheerful",
|
|
1012
|
+
"assistant"
|
|
1013
|
+
]
|
|
1014
|
+
},
|
|
1015
|
+
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
1016
|
+
];
|
|
1017
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1018
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1019
|
+
"characters",
|
|
1020
|
+
"spell-out",
|
|
1021
|
+
"cardinal",
|
|
1022
|
+
"ordinal",
|
|
1023
|
+
"number",
|
|
1024
|
+
"date",
|
|
1025
|
+
"time",
|
|
1026
|
+
"telephone",
|
|
1027
|
+
"fraction",
|
|
1028
|
+
"address",
|
|
1029
|
+
"name",
|
|
1030
|
+
"currency",
|
|
1031
|
+
"number_digit"
|
|
1032
|
+
]);
|
|
1033
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
1034
|
+
"Girl",
|
|
1035
|
+
"Boy",
|
|
1036
|
+
"YoungAdultFemale",
|
|
1037
|
+
"YoungAdultMale",
|
|
1038
|
+
"OlderAdultFemale",
|
|
1039
|
+
"OlderAdultMale",
|
|
1040
|
+
"SeniorFemale",
|
|
1041
|
+
"SeniorMale"
|
|
1042
|
+
]);
|
|
1043
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
1044
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
1045
|
+
"Leading",
|
|
1046
|
+
"Tailing",
|
|
1047
|
+
"Sentenceboundary",
|
|
1048
|
+
"Comma",
|
|
1049
|
+
"Semicolon",
|
|
1050
|
+
"Enumerationcomma"
|
|
1051
|
+
]);
|
|
1052
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
1053
|
+
function decodeAttribute(value) {
|
|
1054
|
+
return value.replace(
|
|
1055
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
1056
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
function findTagEnd2(source, start) {
|
|
1060
|
+
let quote = "";
|
|
1061
|
+
for (let index = start; index < source.length; index += 1) {
|
|
1062
|
+
const character = source[index];
|
|
1063
|
+
if (quote) {
|
|
1064
|
+
if (character === quote) quote = "";
|
|
1065
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
1066
|
+
else if (character === ">") return index;
|
|
1067
|
+
}
|
|
1068
|
+
return source.length - 1;
|
|
1069
|
+
}
|
|
1070
|
+
function tokenizeElements(source) {
|
|
1071
|
+
const tokens = [];
|
|
1072
|
+
const openElements = [];
|
|
1073
|
+
let index = 0;
|
|
1074
|
+
while (index < source.length) {
|
|
1075
|
+
const start = source.indexOf("<", index);
|
|
1076
|
+
if (start === -1) break;
|
|
1077
|
+
if (source.startsWith("<!--", start)) {
|
|
1078
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
1079
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
1083
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
1084
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (source.startsWith("<?", start)) {
|
|
1088
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
1089
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const end = findTagEnd2(source, start + 1);
|
|
1093
|
+
const raw = source.slice(start, end + 1);
|
|
1094
|
+
if (raw.startsWith("</")) {
|
|
1095
|
+
openElements.pop();
|
|
1096
|
+
index = end + 1;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
1100
|
+
if (!nameMatch?.[1]) {
|
|
1101
|
+
index = end + 1;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1105
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
1106
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1107
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1108
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1109
|
+
}
|
|
1110
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
1111
|
+
const parent = openElements[openElements.length - 1];
|
|
1112
|
+
const childElementIndex = parent?.childElementCount;
|
|
1113
|
+
if (parent) parent.childElementCount += 1;
|
|
1114
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1115
|
+
const tokenName = nameMatch[1];
|
|
1116
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1117
|
+
tokens.push({
|
|
1118
|
+
attributes,
|
|
1119
|
+
childElementIndex,
|
|
1120
|
+
end,
|
|
1121
|
+
name: tokenName,
|
|
1122
|
+
parentName: parent?.name,
|
|
1123
|
+
parentVoiceName,
|
|
1124
|
+
selfClosing,
|
|
1125
|
+
start
|
|
1126
|
+
});
|
|
1127
|
+
if (!selfClosing) {
|
|
1128
|
+
openElements.push({
|
|
1129
|
+
childElementCount: 0,
|
|
1130
|
+
name: tokenName,
|
|
1131
|
+
voiceName: tokenVoiceName
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
index = end + 1;
|
|
1135
|
+
}
|
|
1136
|
+
return tokens;
|
|
1137
|
+
}
|
|
1138
|
+
function location(source, offset) {
|
|
1139
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
1140
|
+
const line = before.split("\n").length;
|
|
1141
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
1142
|
+
}
|
|
1143
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
|
|
1144
|
+
diagnostics.push({
|
|
1145
|
+
...location(source, offset),
|
|
1146
|
+
message,
|
|
1147
|
+
severity,
|
|
1148
|
+
source: "ssml-static-validator",
|
|
1149
|
+
...code2 ? { code: code2 } : {}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function isSupportedProsodyRate(value) {
|
|
1153
|
+
const trimmed = value.trim();
|
|
1154
|
+
if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
|
|
1155
|
+
const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
|
|
1156
|
+
if (!multiplier) return false;
|
|
1157
|
+
const numericValue = Number(multiplier[1]);
|
|
1158
|
+
return numericValue >= 0.5 && numericValue <= 2;
|
|
1159
|
+
}
|
|
1160
|
+
function isValidAzureAudioDuration(value) {
|
|
1161
|
+
const trimmed = value.trim();
|
|
1162
|
+
const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
|
|
1163
|
+
if (numeric) return Number(numeric[1]) > 0;
|
|
1164
|
+
const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
|
|
1165
|
+
if (!clock) return false;
|
|
1166
|
+
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
1167
|
+
}
|
|
1168
|
+
function isValidAzureBackgroundAudioDuration(value) {
|
|
1169
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
|
|
1170
|
+
if (!match) return false;
|
|
1171
|
+
const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
|
|
1172
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
|
|
1173
|
+
}
|
|
1174
|
+
function attr(token, name) {
|
|
1175
|
+
return token.attributes.get(name.toLowerCase());
|
|
1176
|
+
}
|
|
1177
|
+
var DEFAULT_LANGUAGE_ALIASES = {
|
|
1178
|
+
"zh-CN": ["zh-Hans"],
|
|
1179
|
+
"zh-TW": ["zh-Hant"]
|
|
1180
|
+
};
|
|
1181
|
+
function canonicalLanguageTag(language) {
|
|
1182
|
+
const trimmed = language.trim();
|
|
1183
|
+
if (!trimmed) return "";
|
|
1184
|
+
try {
|
|
1185
|
+
return new Intl.Locale(trimmed).toString().toLowerCase();
|
|
1186
|
+
} catch {
|
|
1187
|
+
return trimmed.toLowerCase();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function createLanguageNormalizer(options) {
|
|
1191
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1192
|
+
const addAliasGroup = (canonical, values) => {
|
|
1193
|
+
const normalizedCanonical = canonicalLanguageTag(canonical);
|
|
1194
|
+
if (!normalizedCanonical) return;
|
|
1195
|
+
aliases.set(normalizedCanonical, normalizedCanonical);
|
|
1196
|
+
for (const value of values) {
|
|
1197
|
+
const normalizedValue = canonicalLanguageTag(value);
|
|
1198
|
+
if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
|
|
1202
|
+
for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
|
|
1203
|
+
addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
|
|
1204
|
+
return (language) => {
|
|
1205
|
+
const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
|
|
1206
|
+
const normalized = canonicalLanguageTag(customValue);
|
|
1207
|
+
return aliases.get(normalized) ?? normalized;
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function voiceLocalePrefix(voiceName) {
|
|
1211
|
+
const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
|
|
1212
|
+
if (!match?.groups) return void 0;
|
|
1213
|
+
const tag = `${match.groups.language}-${match.groups.region}`;
|
|
1214
|
+
return {
|
|
1215
|
+
language: match.groups.language.toLowerCase(),
|
|
1216
|
+
region: match.groups.region.toLowerCase(),
|
|
1217
|
+
tag
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function definitionFromStyleMap(voiceName, styles) {
|
|
1221
|
+
return {
|
|
1222
|
+
name: voiceName,
|
|
1223
|
+
locale: voiceLocalePrefix(voiceName)?.tag ?? "",
|
|
1224
|
+
styles
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeVoiceCatalog(options) {
|
|
1228
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
|
|
1230
|
+
for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
1231
|
+
for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
1232
|
+
for (const definition of options.customVoiceDefinitions ?? [])
|
|
1233
|
+
definitions.set(definition.name.toLowerCase(), definition);
|
|
1234
|
+
for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
|
|
1235
|
+
const key = voiceName.toLowerCase();
|
|
1236
|
+
const current = definitions.get(key);
|
|
1237
|
+
definitions.set(key, {
|
|
1238
|
+
...current ?? definitionFromStyleMap(voiceName, styles),
|
|
1239
|
+
name: current?.name ?? voiceName,
|
|
1240
|
+
styles: styles.map((style) => style.toLowerCase())
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
return definitions;
|
|
1244
|
+
}
|
|
1245
|
+
function diagnosticSeverity(policy) {
|
|
1246
|
+
if (policy === "ignore") return void 0;
|
|
1247
|
+
return policy === "error" ? "error" : "warning";
|
|
1248
|
+
}
|
|
1249
|
+
function languagePart(language) {
|
|
1250
|
+
try {
|
|
1251
|
+
return new Intl.Locale(language).language.toLowerCase();
|
|
1252
|
+
} catch {
|
|
1253
|
+
return language.split("-")[0]?.toLowerCase() ?? "";
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
|
|
1257
|
+
const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
|
|
1258
|
+
if (candidateLanguages.length === 0 || !language.trim()) return void 0;
|
|
1259
|
+
const normalizedLanguage = normalizeLanguage(language);
|
|
1260
|
+
const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
|
|
1261
|
+
if (normalizedCandidates.includes(normalizedLanguage)) return true;
|
|
1262
|
+
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1263
|
+
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1264
|
+
}
|
|
1265
|
+
function canonicalTagName(name) {
|
|
1266
|
+
const normalized = name.toLowerCase();
|
|
1267
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1268
|
+
if (normalized === "sayas") return "say-as";
|
|
1269
|
+
return normalized;
|
|
1270
|
+
}
|
|
1271
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1272
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1273
|
+
return;
|
|
1274
|
+
const tagName = canonicalTagName(token.name);
|
|
1275
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1276
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1277
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1278
|
+
addDiagnostic(
|
|
1279
|
+
diagnostics,
|
|
1280
|
+
source,
|
|
1281
|
+
token.start,
|
|
1282
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1283
|
+
"error",
|
|
1284
|
+
"azure-unsupported-tag-for-voice"
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1289
|
+
const src = attr(token, "src");
|
|
1290
|
+
if (!src) {
|
|
1291
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
let parsed;
|
|
1295
|
+
try {
|
|
1296
|
+
parsed = new URL(src);
|
|
1297
|
+
} catch {
|
|
1298
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1302
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1303
|
+
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1304
|
+
try {
|
|
1305
|
+
return new URL(allowedOrigin).origin === parsed.origin;
|
|
1306
|
+
} catch {
|
|
1307
|
+
return allowedOrigin === parsed.origin;
|
|
1308
|
+
}
|
|
1309
|
+
}) ?? false;
|
|
1310
|
+
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
1311
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1312
|
+
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
1313
|
+
addDiagnostic(
|
|
1314
|
+
diagnostics,
|
|
1315
|
+
source,
|
|
1316
|
+
token.start,
|
|
1317
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1321
|
+
const name = token.name.toLowerCase();
|
|
1322
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
1323
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
1324
|
+
if (name === "break") {
|
|
1325
|
+
const time = attr(token, "time");
|
|
1326
|
+
const strength = attr(token, "strength");
|
|
1327
|
+
if (!time && !strength)
|
|
1328
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
1329
|
+
if (time && strength)
|
|
1330
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
1331
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
1332
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
1333
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
1334
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
1335
|
+
}
|
|
1336
|
+
if (name === "prosody") {
|
|
1337
|
+
const rate = attr(token, "rate");
|
|
1338
|
+
const pitch = attr(token, "pitch");
|
|
1339
|
+
const volume = attr(token, "volume");
|
|
1340
|
+
if (rate && !isSupportedProsodyRate(rate))
|
|
1341
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
1342
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
1343
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
1344
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
1345
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
1346
|
+
}
|
|
1347
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
1348
|
+
const style = attr(token, "style");
|
|
1349
|
+
if (!style?.trim())
|
|
1350
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
1351
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
1352
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
1353
|
+
addDiagnostic(
|
|
1354
|
+
diagnostics,
|
|
1355
|
+
source,
|
|
1356
|
+
token.start,
|
|
1357
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
1358
|
+
);
|
|
1359
|
+
const role = attr(token, "role");
|
|
1360
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
1361
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
1362
|
+
const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
|
|
1363
|
+
const supportedStyles = definition?.styles;
|
|
1364
|
+
const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
|
|
1365
|
+
if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
|
|
1366
|
+
addDiagnostic(
|
|
1367
|
+
diagnostics,
|
|
1368
|
+
source,
|
|
1369
|
+
token.start,
|
|
1370
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
1371
|
+
severity,
|
|
1372
|
+
"azure-unsupported-style"
|
|
1373
|
+
);
|
|
1374
|
+
if (style && voiceName && !definition && severity)
|
|
1375
|
+
addDiagnostic(
|
|
1376
|
+
diagnostics,
|
|
1377
|
+
source,
|
|
1378
|
+
token.start,
|
|
1379
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
1380
|
+
severity
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
if (name === "say-as" || name === "sayas") {
|
|
1384
|
+
const interpretAs = attr(token, "interpret-as");
|
|
1385
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
1386
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
1387
|
+
}
|
|
1388
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
1389
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
1390
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
1391
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
1392
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
1393
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
1394
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
1395
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
1396
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
1397
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
1398
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
1399
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
1400
|
+
if (name === "lexicon") {
|
|
1401
|
+
const uri = attr(token, "uri");
|
|
1402
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
1403
|
+
else {
|
|
1404
|
+
try {
|
|
1405
|
+
const parsed = new URL(uri);
|
|
1406
|
+
if (parsed.protocol !== "https:")
|
|
1407
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
1408
|
+
} catch {
|
|
1409
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (name === "mstts:silence") {
|
|
1414
|
+
const type = attr(token, "type");
|
|
1415
|
+
const value = attr(token, "value");
|
|
1416
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
1417
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
1418
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
1419
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
1420
|
+
}
|
|
1421
|
+
if (name === "mstts:audioduration") {
|
|
1422
|
+
const value = attr(token, "value");
|
|
1423
|
+
if (!value || !isValidAzureAudioDuration(value))
|
|
1424
|
+
addDiagnostic(
|
|
1425
|
+
diagnostics,
|
|
1426
|
+
source,
|
|
1427
|
+
token.start,
|
|
1428
|
+
'<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
|
|
1429
|
+
);
|
|
1430
|
+
if (!token.selfClosing)
|
|
1431
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
|
|
1432
|
+
}
|
|
1433
|
+
if (name === "mstts:viseme") {
|
|
1434
|
+
const type = attr(token, "type");
|
|
1435
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
1436
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1437
|
+
}
|
|
1438
|
+
if (name === "audio") {
|
|
1439
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1440
|
+
}
|
|
1441
|
+
if (name === "mstts:turn") {
|
|
1442
|
+
if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
|
|
1443
|
+
addDiagnostic(
|
|
1444
|
+
diagnostics,
|
|
1445
|
+
source,
|
|
1446
|
+
token.start,
|
|
1447
|
+
'<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
|
|
1448
|
+
);
|
|
1449
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
1450
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
1451
|
+
}
|
|
1452
|
+
if (name === "mstts:backgroundaudio") {
|
|
1453
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
1454
|
+
const volume = attr(token, "volume");
|
|
1455
|
+
if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
|
|
1456
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
1457
|
+
for (const [attribute, value] of [
|
|
1458
|
+
["fadein", attr(token, "fadein")],
|
|
1459
|
+
["fadeout", attr(token, "fadeout")]
|
|
1460
|
+
]) {
|
|
1461
|
+
if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
|
|
1462
|
+
addDiagnostic(
|
|
1463
|
+
diagnostics,
|
|
1464
|
+
source,
|
|
1465
|
+
token.start,
|
|
1466
|
+
`<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
|
|
1470
|
+
addDiagnostic(
|
|
1471
|
+
diagnostics,
|
|
1472
|
+
source,
|
|
1473
|
+
token.start,
|
|
1474
|
+
"<mstts:backgroundaudio> must be the first element directly under <speak>."
|
|
1475
|
+
);
|
|
1476
|
+
if (!token.selfClosing)
|
|
1477
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
1481
|
+
const diagnostics = [];
|
|
1482
|
+
if (typeof ssml !== "string") {
|
|
1483
|
+
return [
|
|
1484
|
+
{
|
|
1485
|
+
line: 1,
|
|
1486
|
+
column: 1,
|
|
1487
|
+
message: "SSML input must be a string",
|
|
1488
|
+
severity: "error",
|
|
1489
|
+
source: "ssml-static-validator"
|
|
1490
|
+
}
|
|
1491
|
+
];
|
|
1492
|
+
}
|
|
1493
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
1494
|
+
if (ssml.length > maxLength)
|
|
1495
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
1496
|
+
try {
|
|
1497
|
+
parseSsml(ssml);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
1500
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
1501
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
1502
|
+
return diagnostics;
|
|
1503
|
+
}
|
|
1504
|
+
const tokens = tokenizeElements(ssml);
|
|
1505
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
1506
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
1507
|
+
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
1508
|
+
for (const [index, token] of backgroundAudioTokens.entries()) {
|
|
1509
|
+
if (index > 0)
|
|
1510
|
+
addDiagnostic(
|
|
1511
|
+
diagnostics,
|
|
1512
|
+
ssml,
|
|
1513
|
+
token.start,
|
|
1514
|
+
"An SSML document can contain at most one <mstts:backgroundaudio> element."
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
if (!speak || voices.length === 0)
|
|
1518
|
+
addDiagnostic(
|
|
1519
|
+
diagnostics,
|
|
1520
|
+
ssml,
|
|
1521
|
+
speak?.start ?? 0,
|
|
1522
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
1523
|
+
);
|
|
1524
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
1525
|
+
const voiceCatalog = normalizeVoiceCatalog(options);
|
|
1526
|
+
const normalizeLanguage = createLanguageNormalizer(options);
|
|
1527
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
1528
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
1529
|
+
for (const token of voicesToValidate) {
|
|
1530
|
+
const name = attr(token, "name")?.trim();
|
|
1531
|
+
const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
|
|
1532
|
+
const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
|
|
1533
|
+
if (name && !definition && policySeverity)
|
|
1534
|
+
addDiagnostic(
|
|
1535
|
+
diagnostics,
|
|
1536
|
+
ssml,
|
|
1537
|
+
token.start,
|
|
1538
|
+
`Unknown voice "${name}" is not registered in the voice catalog.`,
|
|
1539
|
+
policySeverity,
|
|
1540
|
+
"azure-unknown-voice"
|
|
1541
|
+
);
|
|
1542
|
+
if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
|
|
1543
|
+
addDiagnostic(
|
|
1544
|
+
diagnostics,
|
|
1545
|
+
ssml,
|
|
1546
|
+
token.start,
|
|
1547
|
+
`Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
|
|
1548
|
+
"warning",
|
|
1549
|
+
"azure-locale-mismatch"
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
for (const token of tokens) {
|
|
1553
|
+
const tokenName = token.name.toLowerCase();
|
|
1554
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1555
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
1556
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
1557
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
1558
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
1559
|
+
addDiagnostic(
|
|
1560
|
+
diagnostics,
|
|
1561
|
+
ssml,
|
|
1562
|
+
token.start,
|
|
1563
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
1564
|
+
"error",
|
|
1565
|
+
"azure-unsupported-model-for-voice"
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return diagnostics;
|
|
1570
|
+
}
|
|
1571
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
1572
|
+
apiVersion: "2025-10-01",
|
|
1573
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
1574
|
+
regions: [],
|
|
1575
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
1576
|
+
};
|
|
813
1577
|
|
|
814
1578
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
815
1579
|
function getDocumentChildren(document2) {
|
|
@@ -1948,6 +2712,11 @@ var SSML_ATTRIBUTE_PRESETS = {
|
|
|
1948
2712
|
"mstts:audioduration": {
|
|
1949
2713
|
value: AUDIO_DURATION_PRESETS
|
|
1950
2714
|
},
|
|
2715
|
+
"mstts:backgroundaudio": {
|
|
2716
|
+
volume: PROSODY_VOLUME_PRESETS,
|
|
2717
|
+
fadein: AUDIO_DURATION_PRESETS,
|
|
2718
|
+
fadeout: AUDIO_DURATION_PRESETS
|
|
2719
|
+
},
|
|
1951
2720
|
silence: {
|
|
1952
2721
|
type: SILENCE_TYPE_PRESETS,
|
|
1953
2722
|
value: SILENCE_VALUE_PRESETS
|
|
@@ -2284,7 +3053,7 @@ var SILENCE_VALUE_DESCRIPTIONS = {
|
|
|
2284
3053
|
};
|
|
2285
3054
|
|
|
2286
3055
|
// packages/ssml-editor-react/src/ssmlContext.ts
|
|
2287
|
-
function
|
|
3056
|
+
function findTagEnd3(source, start, limit) {
|
|
2288
3057
|
let quote;
|
|
2289
3058
|
for (let index = start + 1; index < limit; index += 1) {
|
|
2290
3059
|
const character = source[index];
|
|
@@ -2334,7 +3103,7 @@ function findActiveSsmlTags(source, offset) {
|
|
|
2334
3103
|
index = end;
|
|
2335
3104
|
continue;
|
|
2336
3105
|
}
|
|
2337
|
-
const tagEnd =
|
|
3106
|
+
const tagEnd = findTagEnd3(source, tagStart, source.length);
|
|
2338
3107
|
const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
|
|
2339
3108
|
const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
2340
3109
|
const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
@@ -2377,7 +3146,7 @@ function findSsmlVoiceContext(source, offset) {
|
|
|
2377
3146
|
index = end === -1 || end + 2 > limit ? limit : end + 2;
|
|
2378
3147
|
continue;
|
|
2379
3148
|
}
|
|
2380
|
-
const tagEnd =
|
|
3149
|
+
const tagEnd = findTagEnd3(source, tagStart, limit);
|
|
2381
3150
|
if (tagEnd === -1) {
|
|
2382
3151
|
break;
|
|
2383
3152
|
}
|
|
@@ -2439,6 +3208,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
2439
3208
|
label: "mstts:audioduration",
|
|
2440
3209
|
insertText: '<mstts:audioduration value="10s" />'
|
|
2441
3210
|
},
|
|
3211
|
+
{
|
|
3212
|
+
label: "mstts:dialog",
|
|
3213
|
+
insertText: `<mstts:dialog>
|
|
3214
|
+
<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
|
|
3215
|
+
</mstts:dialog>`
|
|
3216
|
+
},
|
|
3217
|
+
{
|
|
3218
|
+
label: "mstts:turn",
|
|
3219
|
+
insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
label: "mstts:backgroundaudio",
|
|
3223
|
+
insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
label: "mstts:ttsembedding",
|
|
3227
|
+
insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
|
|
3228
|
+
},
|
|
2442
3229
|
{
|
|
2443
3230
|
label: "sub",
|
|
2444
3231
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -3184,6 +3971,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
3184
3971
|
example: "10s"
|
|
3185
3972
|
}
|
|
3186
3973
|
]
|
|
3974
|
+
},
|
|
3975
|
+
{
|
|
3976
|
+
name: "mstts:dialog",
|
|
3977
|
+
description: "Groups multiple Azure dialog turns that can use different voices.",
|
|
3978
|
+
parameters: []
|
|
3979
|
+
},
|
|
3980
|
+
{
|
|
3981
|
+
name: "mstts:turn",
|
|
3982
|
+
description: "Adds one dialog turn using the required Azure voice name.",
|
|
3983
|
+
parameters: [
|
|
3984
|
+
{
|
|
3985
|
+
name: "voice",
|
|
3986
|
+
description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
|
|
3987
|
+
example: "en-US-JennyNeural"
|
|
3988
|
+
}
|
|
3989
|
+
]
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
name: "mstts:backgroundaudio",
|
|
3993
|
+
description: "Plays background audio while speech is synthesized.",
|
|
3994
|
+
parameters: [
|
|
3995
|
+
{
|
|
3996
|
+
name: "src",
|
|
3997
|
+
description: "An absolute HTTP(S) URL for the background audio file.",
|
|
3998
|
+
example: "https://example.com/music.mp3"
|
|
3999
|
+
},
|
|
4000
|
+
{
|
|
4001
|
+
name: "volume",
|
|
4002
|
+
description: "The background audio volume, for example `-3dB` or `medium`.",
|
|
4003
|
+
example: "-3dB"
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
name: "fadein",
|
|
4007
|
+
description: "The fade-in duration, for example `1s`.",
|
|
4008
|
+
example: "1s"
|
|
4009
|
+
},
|
|
4010
|
+
{
|
|
4011
|
+
name: "fadeout",
|
|
4012
|
+
description: "The fade-out duration, for example `500ms`.",
|
|
4013
|
+
example: "500ms"
|
|
4014
|
+
}
|
|
4015
|
+
]
|
|
4016
|
+
},
|
|
4017
|
+
{
|
|
4018
|
+
name: "mstts:ttsembedding",
|
|
4019
|
+
description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
|
|
4020
|
+
parameters: []
|
|
4021
|
+
},
|
|
4022
|
+
{
|
|
4023
|
+
name: "mstts:embedding",
|
|
4024
|
+
description: "Specifies embedding metadata for custom voice scenarios.",
|
|
4025
|
+
parameters: []
|
|
4026
|
+
},
|
|
4027
|
+
{
|
|
4028
|
+
name: "mstts:voiceconversion",
|
|
4029
|
+
description: "Specifies voice conversion metadata for custom voice scenarios.",
|
|
4030
|
+
parameters: []
|
|
3187
4031
|
}
|
|
3188
4032
|
];
|
|
3189
4033
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -3242,7 +4086,7 @@ function toRange(source, token) {
|
|
|
3242
4086
|
function containsOffset(token, offset) {
|
|
3243
4087
|
return offset >= token.start && offset < token.end;
|
|
3244
4088
|
}
|
|
3245
|
-
function
|
|
4089
|
+
function findTagEnd4(source, start) {
|
|
3246
4090
|
let quote;
|
|
3247
4091
|
for (let index = start; index < source.length; index += 1) {
|
|
3248
4092
|
const character = source[index];
|
|
@@ -3373,7 +4217,7 @@ function findTagAtOffset(source, offset) {
|
|
|
3373
4217
|
searchStart = tokenEnd2;
|
|
3374
4218
|
continue;
|
|
3375
4219
|
}
|
|
3376
|
-
const tagEnd =
|
|
4220
|
+
const tagEnd = findTagEnd4(source, start + 1);
|
|
3377
4221
|
const contentEnd = tagEnd ?? source.length;
|
|
3378
4222
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
3379
4223
|
if (offset < tokenEnd) {
|
|
@@ -3989,6 +4833,60 @@ section[data-ssml-editor] {
|
|
|
3989
4833
|
border-radius: 0.25rem;
|
|
3990
4834
|
overflow: visible;
|
|
3991
4835
|
}
|
|
4836
|
+
[data-ssml-editor] .ssml-editor-visual {
|
|
4837
|
+
display: grid;
|
|
4838
|
+
gap: 0.75rem;
|
|
4839
|
+
min-height: 8rem;
|
|
4840
|
+
padding: 0.75rem;
|
|
4841
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4842
|
+
border-radius: 0.25rem;
|
|
4843
|
+
}
|
|
4844
|
+
[data-ssml-editor] .ssml-editor-visual-layout {
|
|
4845
|
+
display: grid;
|
|
4846
|
+
grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
|
|
4847
|
+
gap: 1rem;
|
|
4848
|
+
}
|
|
4849
|
+
[data-ssml-editor] .ssml-editor-visual-tree ul {
|
|
4850
|
+
margin: 0;
|
|
4851
|
+
padding-left: 1.25rem;
|
|
4852
|
+
}
|
|
4853
|
+
[data-ssml-editor] .ssml-editor-visual button {
|
|
4854
|
+
padding: 0.35rem 0.5rem;
|
|
4855
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4856
|
+
border-radius: 0.25rem;
|
|
4857
|
+
color: var(--ssml-editor-color);
|
|
4858
|
+
background: var(--ssml-editor-control-bg);
|
|
4859
|
+
cursor: pointer;
|
|
4860
|
+
}
|
|
4861
|
+
[data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
|
|
4862
|
+
border-color: var(--ssml-editor-active-border);
|
|
4863
|
+
background: var(--ssml-editor-active-bg);
|
|
4864
|
+
}
|
|
4865
|
+
[data-ssml-editor] .ssml-editor-visual textarea {
|
|
4866
|
+
box-sizing: border-box;
|
|
4867
|
+
width: 100%;
|
|
4868
|
+
min-height: 6rem;
|
|
4869
|
+
padding: 0.5rem;
|
|
4870
|
+
color: var(--ssml-editor-color);
|
|
4871
|
+
background: var(--ssml-editor-control-bg);
|
|
4872
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4873
|
+
border-radius: 0.25rem;
|
|
4874
|
+
font: inherit;
|
|
4875
|
+
}
|
|
4876
|
+
[data-ssml-editor] .ssml-editor-visual-actions,
|
|
4877
|
+
[data-ssml-editor] .ssml-editor-visual-breadcrumb {
|
|
4878
|
+
display: flex;
|
|
4879
|
+
flex-wrap: wrap;
|
|
4880
|
+
gap: 0.4rem;
|
|
4881
|
+
align-items: center;
|
|
4882
|
+
}
|
|
4883
|
+
[data-ssml-editor] .ssml-editor-visual-errors {
|
|
4884
|
+
padding: 0.5rem;
|
|
4885
|
+
color: #b91c1c;
|
|
4886
|
+
background: #fef2f2;
|
|
4887
|
+
border: 1px solid #b91c1c;
|
|
4888
|
+
border-radius: 0.25rem;
|
|
4889
|
+
}
|
|
3992
4890
|
`.trim();
|
|
3993
4891
|
function injectStyles() {
|
|
3994
4892
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
|
|
@@ -4002,6 +4900,55 @@ function injectStyles() {
|
|
|
4002
4900
|
function isDarkTheme(theme) {
|
|
4003
4901
|
return theme === "vs-dark" || theme.toLowerCase().includes("dark");
|
|
4004
4902
|
}
|
|
4903
|
+
function isElement(node) {
|
|
4904
|
+
return typeof node !== "string" && node.type !== "text";
|
|
4905
|
+
}
|
|
4906
|
+
function visualElementName(element) {
|
|
4907
|
+
return element.type === "custom" || element.type === "element" ? element.name : element.type;
|
|
4908
|
+
}
|
|
4909
|
+
function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
|
|
4910
|
+
return nodes.flatMap((node, index) => {
|
|
4911
|
+
const currentPath = [...path, index];
|
|
4912
|
+
if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
|
|
4913
|
+
if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
|
|
4914
|
+
return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
|
|
4915
|
+
});
|
|
4916
|
+
}
|
|
4917
|
+
function updateVisualNodes(nodes, path, update) {
|
|
4918
|
+
if (path.length === 0) return nodes;
|
|
4919
|
+
const [index, ...rest] = path;
|
|
4920
|
+
return nodes.flatMap((node, nodeIndex) => {
|
|
4921
|
+
if (nodeIndex !== index) return [node];
|
|
4922
|
+
if (rest.length === 0) {
|
|
4923
|
+
const next = update(node);
|
|
4924
|
+
return Array.isArray(next) ? next : [next];
|
|
4925
|
+
}
|
|
4926
|
+
if (!isElement(node)) return [node];
|
|
4927
|
+
return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
|
|
4928
|
+
});
|
|
4929
|
+
}
|
|
4930
|
+
function updateVisualText(document2, path, value) {
|
|
4931
|
+
return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
|
|
4932
|
+
}
|
|
4933
|
+
function wrapVisualText(document2, path, start, end, type, attributes) {
|
|
4934
|
+
return {
|
|
4935
|
+
...document2,
|
|
4936
|
+
children: updateVisualNodes(document2.children ?? [], path, (node) => {
|
|
4937
|
+
const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
|
|
4938
|
+
if (!value || start === end) return node;
|
|
4939
|
+
if (type === "break") {
|
|
4940
|
+
return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
|
|
4941
|
+
(part) => typeof part === "string" ? part.length > 0 : true
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4944
|
+
const selected = value.slice(start, end);
|
|
4945
|
+
const wrapper = { type, attributes, children: [selected] };
|
|
4946
|
+
return [value.slice(0, start), wrapper, value.slice(end)].filter(
|
|
4947
|
+
(part) => typeof part === "string" ? part.length > 0 : true
|
|
4948
|
+
);
|
|
4949
|
+
})
|
|
4950
|
+
};
|
|
4951
|
+
}
|
|
4005
4952
|
function getMenuPosition(trigger, menu) {
|
|
4006
4953
|
const bounds = trigger.getBoundingClientRect();
|
|
4007
4954
|
const margin = 8;
|
|
@@ -4023,6 +4970,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4023
4970
|
this.toolbarActions = null;
|
|
4024
4971
|
this.display = null;
|
|
4025
4972
|
this.editorContainer = null;
|
|
4973
|
+
this.visualContainer = null;
|
|
4026
4974
|
this.helpPanel = null;
|
|
4027
4975
|
this.openMenu = null;
|
|
4028
4976
|
this.openMenuTrigger = null;
|
|
@@ -4036,6 +4984,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4036
4984
|
this.documentState = null;
|
|
4037
4985
|
this.decorationsVisible = false;
|
|
4038
4986
|
this.helpOpen = false;
|
|
4987
|
+
this.visualSelectedPath = null;
|
|
4988
|
+
this.visualSelection = { start: 0, end: 0 };
|
|
4039
4989
|
this.handleDocumentPointerDown = (event) => {
|
|
4040
4990
|
const target = event.target;
|
|
4041
4991
|
if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
|
|
@@ -4077,6 +5027,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4077
5027
|
set locale(locale) {
|
|
4078
5028
|
this.setAttribute("locale", locale);
|
|
4079
5029
|
}
|
|
5030
|
+
get editMode() {
|
|
5031
|
+
return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
|
|
5032
|
+
}
|
|
5033
|
+
set editMode(mode) {
|
|
5034
|
+
this.setAttribute("edit-mode", mode);
|
|
5035
|
+
}
|
|
4080
5036
|
prepareDocument(value) {
|
|
4081
5037
|
try {
|
|
4082
5038
|
this.documentState = parseSsml(value);
|
|
@@ -4119,6 +5075,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4119
5075
|
}
|
|
4120
5076
|
}
|
|
4121
5077
|
}
|
|
5078
|
+
this.renderVisualEditor();
|
|
4122
5079
|
return;
|
|
4123
5080
|
}
|
|
4124
5081
|
if (name === "theme" && this.monaco) {
|
|
@@ -4136,6 +5093,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4136
5093
|
this.renderHelp();
|
|
4137
5094
|
return;
|
|
4138
5095
|
}
|
|
5096
|
+
if (name === "edit-mode") {
|
|
5097
|
+
this.updateEditMode();
|
|
5098
|
+
}
|
|
4139
5099
|
if (name === "show-decorations") {
|
|
4140
5100
|
this.decorationsVisible = newValue !== null;
|
|
4141
5101
|
this.updateDecorations();
|
|
@@ -4160,7 +5120,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4160
5120
|
display.dataset.ssmlEditorDisplay = "";
|
|
4161
5121
|
const editorContainer = document.createElement("div");
|
|
4162
5122
|
editorContainer.className = "ssml-editor-editor";
|
|
4163
|
-
|
|
5123
|
+
const visualContainer = document.createElement("div");
|
|
5124
|
+
visualContainer.className = "ssml-editor-visual";
|
|
5125
|
+
display.append(editorContainer, visualContainer);
|
|
4164
5126
|
root.append(toolbar, display);
|
|
4165
5127
|
this.replaceChildren(root);
|
|
4166
5128
|
this.root = root;
|
|
@@ -4168,8 +5130,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4168
5130
|
this.toolbarActions = toolbarActions;
|
|
4169
5131
|
this.display = display;
|
|
4170
5132
|
this.editorContainer = editorContainer;
|
|
5133
|
+
this.visualContainer = visualContainer;
|
|
4171
5134
|
this.renderToolbar();
|
|
4172
5135
|
this.renderHelp();
|
|
5136
|
+
this.updateEditMode();
|
|
5137
|
+
this.renderVisualEditor();
|
|
4173
5138
|
}
|
|
4174
5139
|
renderToolbar() {
|
|
4175
5140
|
const toolbar = this.toolbar;
|
|
@@ -4213,10 +5178,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4213
5178
|
for (const id of toolbarIds) {
|
|
4214
5179
|
const group = groupByButtonId.get(id);
|
|
4215
5180
|
if (previousGroup !== void 0 && group !== previousGroup) {
|
|
4216
|
-
const
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
toolbarActions.append(
|
|
5181
|
+
const separator2 = document.createElement("span");
|
|
5182
|
+
separator2.className = "ssml-editor-toolbar-separator";
|
|
5183
|
+
separator2.setAttribute("aria-hidden", "true");
|
|
5184
|
+
toolbarActions.append(separator2);
|
|
4220
5185
|
}
|
|
4221
5186
|
previousGroup = group;
|
|
4222
5187
|
const insertion = insertionById.get(id);
|
|
@@ -4228,8 +5193,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4228
5193
|
toolbarActions.append(this.createActionButton(id));
|
|
4229
5194
|
}
|
|
4230
5195
|
}
|
|
5196
|
+
const separator = document.createElement("span");
|
|
5197
|
+
separator.className = "ssml-editor-toolbar-separator";
|
|
5198
|
+
separator.setAttribute("aria-hidden", "true");
|
|
5199
|
+
toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
|
|
4231
5200
|
this.updateActiveButtons();
|
|
4232
5201
|
}
|
|
5202
|
+
createModeButton(mode, label) {
|
|
5203
|
+
const button = document.createElement("button");
|
|
5204
|
+
button.type = "button";
|
|
5205
|
+
button.className = "ssml-editor-toolbar-button";
|
|
5206
|
+
button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
|
|
5207
|
+
button.setAttribute("aria-pressed", String(this.editMode === mode));
|
|
5208
|
+
button.textContent = label;
|
|
5209
|
+
button.addEventListener("click", () => {
|
|
5210
|
+
this.editMode = mode;
|
|
5211
|
+
});
|
|
5212
|
+
return button;
|
|
5213
|
+
}
|
|
4233
5214
|
createActionButton(id) {
|
|
4234
5215
|
const copy = EDITOR_COPY[this.locale];
|
|
4235
5216
|
const labels = {
|
|
@@ -4609,6 +5590,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4609
5590
|
this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
|
|
4610
5591
|
}
|
|
4611
5592
|
}
|
|
5593
|
+
updateEditMode() {
|
|
5594
|
+
if (this.editorContainer && this.visualContainer) {
|
|
5595
|
+
const visual = this.editMode === "visual";
|
|
5596
|
+
this.editorContainer.hidden = visual;
|
|
5597
|
+
this.visualContainer.hidden = !visual;
|
|
5598
|
+
}
|
|
5599
|
+
for (const mode of ["visual", "code"]) {
|
|
5600
|
+
const button = this.toolbarActions?.querySelector(
|
|
5601
|
+
`[data-ssml-editor-button="edit-mode-${mode}"]`
|
|
5602
|
+
);
|
|
5603
|
+
button?.setAttribute("aria-pressed", String(this.editMode === mode));
|
|
5604
|
+
button?.toggleAttribute("data-active", this.editMode === mode);
|
|
5605
|
+
}
|
|
5606
|
+
this.renderVisualEditor();
|
|
5607
|
+
}
|
|
5608
|
+
renderVisualEditor() {
|
|
5609
|
+
const container = this.visualContainer;
|
|
5610
|
+
if (!container) return;
|
|
5611
|
+
container.replaceChildren();
|
|
5612
|
+
if (!this.documentState) {
|
|
5613
|
+
const error = document.createElement("p");
|
|
5614
|
+
error.className = "ssml-editor-visual-errors";
|
|
5615
|
+
error.textContent = "SSML syntax must be valid before visual editing is available.";
|
|
5616
|
+
container.append(error);
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5619
|
+
const documentState = this.documentState;
|
|
5620
|
+
const leaves = collectVisualTextLeaves(documentState.children ?? []);
|
|
5621
|
+
const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
|
|
5622
|
+
const breadcrumb = document.createElement("div");
|
|
5623
|
+
breadcrumb.className = "ssml-editor-visual-breadcrumb";
|
|
5624
|
+
breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
|
|
5625
|
+
const clear = document.createElement("button");
|
|
5626
|
+
clear.type = "button";
|
|
5627
|
+
clear.textContent = "Clear parent";
|
|
5628
|
+
clear.disabled = !this.visualSelectedPath;
|
|
5629
|
+
clear.addEventListener("click", () => {
|
|
5630
|
+
this.visualSelectedPath = null;
|
|
5631
|
+
this.renderVisualEditor();
|
|
5632
|
+
});
|
|
5633
|
+
breadcrumb.append(clear);
|
|
5634
|
+
container.append(breadcrumb);
|
|
5635
|
+
const diagnostics = validateAzureSsml(buildSsml(documentState));
|
|
5636
|
+
if (diagnostics.length > 0) {
|
|
5637
|
+
const errors = document.createElement("div");
|
|
5638
|
+
errors.className = "ssml-editor-visual-errors";
|
|
5639
|
+
errors.setAttribute("role", "alert");
|
|
5640
|
+
for (const diagnostic of diagnostics) {
|
|
5641
|
+
const message = document.createElement("div");
|
|
5642
|
+
message.textContent = diagnostic.message;
|
|
5643
|
+
errors.append(message);
|
|
5644
|
+
}
|
|
5645
|
+
container.append(errors);
|
|
5646
|
+
}
|
|
5647
|
+
const layout = document.createElement("div");
|
|
5648
|
+
layout.className = "ssml-editor-visual-layout";
|
|
5649
|
+
const tree = document.createElement("nav");
|
|
5650
|
+
tree.className = "ssml-editor-visual-tree";
|
|
5651
|
+
tree.setAttribute("aria-label", "SSML structure tree");
|
|
5652
|
+
tree.append(document.createTextNode("Structure"));
|
|
5653
|
+
const treeList = document.createElement("ul");
|
|
5654
|
+
const renderTree = (nodes, parent, parentPath = []) => {
|
|
5655
|
+
nodes.forEach((node, index) => {
|
|
5656
|
+
if (!isElement(node)) return;
|
|
5657
|
+
const path = [...parentPath, index];
|
|
5658
|
+
const item = document.createElement("li");
|
|
5659
|
+
const button = document.createElement("button");
|
|
5660
|
+
button.type = "button";
|
|
5661
|
+
button.textContent = `<${visualElementName(node)}>`;
|
|
5662
|
+
button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
|
|
5663
|
+
button.addEventListener("click", () => {
|
|
5664
|
+
this.visualSelectedPath = path;
|
|
5665
|
+
this.renderVisualEditor();
|
|
5666
|
+
});
|
|
5667
|
+
item.append(button);
|
|
5668
|
+
if ((node.children ?? []).some(isElement)) {
|
|
5669
|
+
const childList = document.createElement("ul");
|
|
5670
|
+
renderTree(node.children ?? [], childList, path);
|
|
5671
|
+
item.append(childList);
|
|
5672
|
+
}
|
|
5673
|
+
parent.append(item);
|
|
5674
|
+
});
|
|
5675
|
+
};
|
|
5676
|
+
renderTree(documentState.children ?? [], treeList);
|
|
5677
|
+
tree.append(treeList);
|
|
5678
|
+
layout.append(tree);
|
|
5679
|
+
const form = document.createElement("div");
|
|
5680
|
+
form.className = "ssml-editor-visual-form";
|
|
5681
|
+
if (selectedLeaf) {
|
|
5682
|
+
const label = document.createElement("label");
|
|
5683
|
+
label.append(document.createTextNode("Text"));
|
|
5684
|
+
const textarea = document.createElement("textarea");
|
|
5685
|
+
textarea.value = selectedLeaf.value;
|
|
5686
|
+
textarea.readOnly = this.readonly;
|
|
5687
|
+
textarea.addEventListener("select", () => {
|
|
5688
|
+
this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
|
|
5689
|
+
});
|
|
5690
|
+
textarea.addEventListener("input", () => {
|
|
5691
|
+
if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
|
|
5692
|
+
});
|
|
5693
|
+
label.append(textarea);
|
|
5694
|
+
form.append(label);
|
|
5695
|
+
const actions = document.createElement("div");
|
|
5696
|
+
actions.className = "ssml-editor-visual-actions";
|
|
5697
|
+
const wrappers = [
|
|
5698
|
+
["Rate", "prosody", { rate: "slow" }],
|
|
5699
|
+
["Pitch", "prosody", { pitch: "high" }],
|
|
5700
|
+
["Emotion", "mstts:express-as", { style: "cheerful" }],
|
|
5701
|
+
["Pause", "break", { time: "500ms" }],
|
|
5702
|
+
["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
|
|
5703
|
+
];
|
|
5704
|
+
for (const [labelText, type, attributes] of wrappers) {
|
|
5705
|
+
const button = document.createElement("button");
|
|
5706
|
+
button.type = "button";
|
|
5707
|
+
button.textContent = labelText;
|
|
5708
|
+
button.disabled = this.readonly;
|
|
5709
|
+
button.addEventListener("click", () => {
|
|
5710
|
+
const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
|
|
5711
|
+
const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
|
|
5712
|
+
this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
|
|
5713
|
+
this.renderVisualEditor();
|
|
5714
|
+
});
|
|
5715
|
+
actions.append(button);
|
|
5716
|
+
}
|
|
5717
|
+
const preview = document.createElement("button");
|
|
5718
|
+
preview.type = "button";
|
|
5719
|
+
preview.textContent = "Preview selection";
|
|
5720
|
+
preview.addEventListener("click", () => {
|
|
5721
|
+
this.dispatchEvent(
|
|
5722
|
+
new CustomEvent("preview-selection", {
|
|
5723
|
+
detail: { ssml: buildSsml({ ...documentState, children: [selectedLeaf.value] }) },
|
|
5724
|
+
bubbles: true,
|
|
5725
|
+
composed: true
|
|
5726
|
+
})
|
|
5727
|
+
);
|
|
5728
|
+
});
|
|
5729
|
+
actions.append(preview);
|
|
5730
|
+
form.append(actions);
|
|
5731
|
+
} else {
|
|
5732
|
+
form.textContent = "Select an element or text node to edit it.";
|
|
5733
|
+
}
|
|
5734
|
+
layout.append(form);
|
|
5735
|
+
container.append(layout);
|
|
5736
|
+
}
|
|
4612
5737
|
updateActiveButtons() {
|
|
4613
5738
|
const editor = this.editor;
|
|
4614
5739
|
const model = this.model;
|
|
@@ -4636,6 +5761,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4636
5761
|
return;
|
|
4637
5762
|
}
|
|
4638
5763
|
const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
|
|
5764
|
+
this.renderVisualEditor();
|
|
4639
5765
|
const editor = monacoModule.editor.create(container, {
|
|
4640
5766
|
model,
|
|
4641
5767
|
theme: this.theme,
|
|
@@ -4734,7 +5860,8 @@ SsmlEditorElement.observedAttributes = [
|
|
|
4734
5860
|
"locale",
|
|
4735
5861
|
"show-toolbar",
|
|
4736
5862
|
"show-toolbar-labels",
|
|
4737
|
-
"show-decorations"
|
|
5863
|
+
"show-decorations",
|
|
5864
|
+
"edit-mode"
|
|
4738
5865
|
];
|
|
4739
5866
|
|
|
4740
5867
|
// packages/ssml-editor-elements/src/index.ts
|