ssml-builder-js 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -8
- package/dist/{chunk-VCDMKVVT.mjs → chunk-7LCSH4SZ.mjs} +457 -25
- package/dist/chunk-7LCSH4SZ.mjs.map +1 -0
- package/dist/{chunk-MWSDFGXW.mjs → chunk-JNJVTEL6.mjs} +785 -9
- package/dist/chunk-JNJVTEL6.mjs.map +1 -0
- package/dist/core.d.mts +91 -3
- package/dist/core.d.ts +91 -3
- package/dist/core.js +462 -25
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +13 -3
- package/dist/elements.d.mts +10 -1
- package/dist/elements.d.ts +10 -1
- package/dist/elements.js +1071 -13
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +292 -8
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +462 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +40 -3
- package/dist/react.d.ts +40 -3
- package/dist/react.js +1026 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +237 -5
- package/dist/react.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-MWSDFGXW.mjs.map +0 -1
- package/dist/chunk-VCDMKVVT.mjs.map +0 -1
package/dist/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,7 @@ var SSML_ATTRS = {
|
|
|
84
90
|
LANG: "lang",
|
|
85
91
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
86
92
|
NAME: "name",
|
|
93
|
+
VOICE: "voice",
|
|
87
94
|
EFFECT: "effect",
|
|
88
95
|
RATE: "rate",
|
|
89
96
|
PITCH: "pitch",
|
|
@@ -115,7 +122,9 @@ var SSML_ATTRS = {
|
|
|
115
122
|
MARK: "mark",
|
|
116
123
|
URI: "uri",
|
|
117
124
|
TYPE: "type",
|
|
118
|
-
VALUE: "value"
|
|
125
|
+
VALUE: "value",
|
|
126
|
+
FADE_IN: "fadein",
|
|
127
|
+
FADE_OUT: "fadeout"
|
|
119
128
|
};
|
|
120
129
|
function escapeText(value) {
|
|
121
130
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -205,6 +214,20 @@ function getAttributes(element) {
|
|
|
205
214
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
206
215
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
207
216
|
break;
|
|
217
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
218
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
219
|
+
break;
|
|
220
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
221
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
222
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
223
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
224
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
225
|
+
break;
|
|
226
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
227
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
228
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
229
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
230
|
+
break;
|
|
208
231
|
case SSML_TAGS.PARAGRAPH:
|
|
209
232
|
case SSML_TAGS.SENTENCE:
|
|
210
233
|
case SSML_TAGS.WORD:
|
|
@@ -769,6 +792,40 @@ function convertElement(node) {
|
|
|
769
792
|
if (value !== void 0) element.value = value;
|
|
770
793
|
return finishElement(element, node, attributes);
|
|
771
794
|
}
|
|
795
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
796
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
797
|
+
return finishElement(element, node, attributes);
|
|
798
|
+
}
|
|
799
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
800
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
801
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
802
|
+
if (voice !== void 0) element.voice = voice;
|
|
803
|
+
return finishElement(element, node, attributes);
|
|
804
|
+
}
|
|
805
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
806
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
807
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
808
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
809
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
810
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
811
|
+
if (src !== void 0) element.src = src;
|
|
812
|
+
if (volume !== void 0) element.volume = volume;
|
|
813
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
814
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
815
|
+
return finishElement(element, node, attributes);
|
|
816
|
+
}
|
|
817
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
818
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
819
|
+
return finishElement(element, node, attributes);
|
|
820
|
+
}
|
|
821
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
822
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
823
|
+
return finishElement(element, node, attributes);
|
|
824
|
+
}
|
|
825
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
826
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
827
|
+
return finishElement(element, node, attributes);
|
|
828
|
+
}
|
|
772
829
|
default: {
|
|
773
830
|
const element = {
|
|
774
831
|
name: node.name,
|
|
@@ -810,6 +867,644 @@ function parseSsml(xmlString) {
|
|
|
810
867
|
}
|
|
811
868
|
return document2;
|
|
812
869
|
}
|
|
870
|
+
var AZURE_VOICE_DEFINITIONS = [
|
|
871
|
+
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
872
|
+
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
873
|
+
{ name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
|
|
874
|
+
{
|
|
875
|
+
name: "en-US-GuyNeural",
|
|
876
|
+
locale: "en-US",
|
|
877
|
+
styles: [
|
|
878
|
+
"angry",
|
|
879
|
+
"cheerful",
|
|
880
|
+
"excited",
|
|
881
|
+
"friendly",
|
|
882
|
+
"hopeful",
|
|
883
|
+
"newscast",
|
|
884
|
+
"sad",
|
|
885
|
+
"shouting",
|
|
886
|
+
"terrified",
|
|
887
|
+
"unfriendly",
|
|
888
|
+
"whispering"
|
|
889
|
+
]
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: "en-US-JennyMultilingualNeural",
|
|
893
|
+
locale: "en-US",
|
|
894
|
+
styles: [
|
|
895
|
+
"cheerful",
|
|
896
|
+
"empathetic",
|
|
897
|
+
"excited",
|
|
898
|
+
"friendly",
|
|
899
|
+
"hopeful",
|
|
900
|
+
"sad",
|
|
901
|
+
"shouting",
|
|
902
|
+
"terrified",
|
|
903
|
+
"unfriendly",
|
|
904
|
+
"whispering"
|
|
905
|
+
]
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: "en-US-JennyNeural",
|
|
909
|
+
locale: "en-US",
|
|
910
|
+
styles: [
|
|
911
|
+
"assistant",
|
|
912
|
+
"chat",
|
|
913
|
+
"customerservice",
|
|
914
|
+
"newscast",
|
|
915
|
+
"cheerful",
|
|
916
|
+
"empathetic",
|
|
917
|
+
"excited",
|
|
918
|
+
"friendly",
|
|
919
|
+
"hopeful",
|
|
920
|
+
"sad",
|
|
921
|
+
"shouting",
|
|
922
|
+
"terrified",
|
|
923
|
+
"unfriendly",
|
|
924
|
+
"whispering"
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
{ name: "es-ES-ElviraNeural", locale: "es-ES" },
|
|
928
|
+
{ name: "fil-PH-AngeloNeural", locale: "fil-PH" },
|
|
929
|
+
{ name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
930
|
+
{ name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
931
|
+
{ name: "id-ID-GadisNeural", locale: "id-ID" },
|
|
932
|
+
{ name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
|
|
933
|
+
{ name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
|
|
934
|
+
{ name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
|
|
935
|
+
{ name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
|
|
936
|
+
{ name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
|
|
937
|
+
{ name: "ms-MY-YasminNeural", locale: "ms-MY" },
|
|
938
|
+
{ name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
|
|
939
|
+
{
|
|
940
|
+
name: "ru-RU-SvetlanaNeural",
|
|
941
|
+
locale: "ru-RU",
|
|
942
|
+
styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
943
|
+
},
|
|
944
|
+
{ name: "th-TH-PremwadeeNeural", locale: "th-TH" },
|
|
945
|
+
{ name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
|
|
946
|
+
{
|
|
947
|
+
name: "zh-CN-XiaoxiaoNeural",
|
|
948
|
+
locale: "zh-CN",
|
|
949
|
+
styles: [
|
|
950
|
+
"assistant",
|
|
951
|
+
"chat",
|
|
952
|
+
"customerservice",
|
|
953
|
+
"newscast",
|
|
954
|
+
"cheerful",
|
|
955
|
+
"empathetic",
|
|
956
|
+
"excited",
|
|
957
|
+
"friendly",
|
|
958
|
+
"hopeful",
|
|
959
|
+
"sad",
|
|
960
|
+
"terrified",
|
|
961
|
+
"whispering",
|
|
962
|
+
"poetry-reading",
|
|
963
|
+
"sports_commentary",
|
|
964
|
+
"sports_commentary_excited",
|
|
965
|
+
"story"
|
|
966
|
+
]
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "zh-CN-YunxiNeural",
|
|
970
|
+
locale: "zh-CN",
|
|
971
|
+
styles: [
|
|
972
|
+
"narration-relaxed",
|
|
973
|
+
"embarrassed",
|
|
974
|
+
"fearful",
|
|
975
|
+
"sad",
|
|
976
|
+
"disgruntled",
|
|
977
|
+
"serious",
|
|
978
|
+
"angry",
|
|
979
|
+
"depressed",
|
|
980
|
+
"chat",
|
|
981
|
+
"cheerful",
|
|
982
|
+
"assistant"
|
|
983
|
+
]
|
|
984
|
+
},
|
|
985
|
+
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
986
|
+
];
|
|
987
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
988
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
989
|
+
"characters",
|
|
990
|
+
"spell-out",
|
|
991
|
+
"cardinal",
|
|
992
|
+
"ordinal",
|
|
993
|
+
"number",
|
|
994
|
+
"date",
|
|
995
|
+
"time",
|
|
996
|
+
"telephone",
|
|
997
|
+
"fraction",
|
|
998
|
+
"address",
|
|
999
|
+
"name",
|
|
1000
|
+
"currency",
|
|
1001
|
+
"number_digit"
|
|
1002
|
+
]);
|
|
1003
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
1004
|
+
"Girl",
|
|
1005
|
+
"Boy",
|
|
1006
|
+
"YoungAdultFemale",
|
|
1007
|
+
"YoungAdultMale",
|
|
1008
|
+
"OlderAdultFemale",
|
|
1009
|
+
"OlderAdultMale",
|
|
1010
|
+
"SeniorFemale",
|
|
1011
|
+
"SeniorMale"
|
|
1012
|
+
]);
|
|
1013
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
1014
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
1015
|
+
"Leading",
|
|
1016
|
+
"Tailing",
|
|
1017
|
+
"Sentenceboundary",
|
|
1018
|
+
"Comma",
|
|
1019
|
+
"Semicolon",
|
|
1020
|
+
"Enumerationcomma"
|
|
1021
|
+
]);
|
|
1022
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
1023
|
+
function decodeAttribute(value) {
|
|
1024
|
+
return value.replace(
|
|
1025
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
1026
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
function findTagEnd2(source, start) {
|
|
1030
|
+
let quote = "";
|
|
1031
|
+
for (let index = start; index < source.length; index += 1) {
|
|
1032
|
+
const character = source[index];
|
|
1033
|
+
if (quote) {
|
|
1034
|
+
if (character === quote) quote = "";
|
|
1035
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
1036
|
+
else if (character === ">") return index;
|
|
1037
|
+
}
|
|
1038
|
+
return source.length - 1;
|
|
1039
|
+
}
|
|
1040
|
+
function tokenizeElements(source) {
|
|
1041
|
+
const tokens = [];
|
|
1042
|
+
const openElements = [];
|
|
1043
|
+
let index = 0;
|
|
1044
|
+
while (index < source.length) {
|
|
1045
|
+
const start = source.indexOf("<", index);
|
|
1046
|
+
if (start === -1) break;
|
|
1047
|
+
if (source.startsWith("<!--", start)) {
|
|
1048
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
1049
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
1053
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
1054
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (source.startsWith("<?", start)) {
|
|
1058
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
1059
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
const end = findTagEnd2(source, start + 1);
|
|
1063
|
+
const raw = source.slice(start, end + 1);
|
|
1064
|
+
if (raw.startsWith("</")) {
|
|
1065
|
+
openElements.pop();
|
|
1066
|
+
index = end + 1;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
1070
|
+
if (!nameMatch?.[1]) {
|
|
1071
|
+
index = end + 1;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1075
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
1076
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1077
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1078
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1079
|
+
}
|
|
1080
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
1081
|
+
const parent = openElements[openElements.length - 1];
|
|
1082
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1083
|
+
const tokenName = nameMatch[1];
|
|
1084
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1085
|
+
tokens.push({
|
|
1086
|
+
attributes,
|
|
1087
|
+
end,
|
|
1088
|
+
name: tokenName,
|
|
1089
|
+
parentName: parent?.name,
|
|
1090
|
+
parentVoiceName,
|
|
1091
|
+
selfClosing,
|
|
1092
|
+
start
|
|
1093
|
+
});
|
|
1094
|
+
if (!selfClosing) {
|
|
1095
|
+
openElements.push({
|
|
1096
|
+
name: tokenName,
|
|
1097
|
+
voiceName: tokenVoiceName
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
index = end + 1;
|
|
1101
|
+
}
|
|
1102
|
+
return tokens;
|
|
1103
|
+
}
|
|
1104
|
+
function location(source, offset) {
|
|
1105
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
1106
|
+
const line = before.split("\n").length;
|
|
1107
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
1108
|
+
}
|
|
1109
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
|
|
1110
|
+
diagnostics.push({
|
|
1111
|
+
...location(source, offset),
|
|
1112
|
+
message,
|
|
1113
|
+
severity,
|
|
1114
|
+
source: "ssml-static-validator",
|
|
1115
|
+
...code2 ? { code: code2 } : {}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
function isSupportedProsodyRate(value) {
|
|
1119
|
+
const trimmed = value.trim();
|
|
1120
|
+
if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
|
|
1121
|
+
const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
|
|
1122
|
+
if (!multiplier) return false;
|
|
1123
|
+
const numericValue = Number(multiplier[1]);
|
|
1124
|
+
return numericValue >= 0.5 && numericValue <= 2;
|
|
1125
|
+
}
|
|
1126
|
+
function isValidAzureAudioDuration(value) {
|
|
1127
|
+
const trimmed = value.trim();
|
|
1128
|
+
const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
|
|
1129
|
+
if (numeric) return Number(numeric[1]) > 0;
|
|
1130
|
+
const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
|
|
1131
|
+
if (!clock) return false;
|
|
1132
|
+
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
1133
|
+
}
|
|
1134
|
+
function attr(token, name) {
|
|
1135
|
+
return token.attributes.get(name.toLowerCase());
|
|
1136
|
+
}
|
|
1137
|
+
var DEFAULT_LANGUAGE_ALIASES = {
|
|
1138
|
+
"zh-CN": ["zh-Hans"],
|
|
1139
|
+
"zh-TW": ["zh-Hant"]
|
|
1140
|
+
};
|
|
1141
|
+
function canonicalLanguageTag(language) {
|
|
1142
|
+
const trimmed = language.trim();
|
|
1143
|
+
if (!trimmed) return "";
|
|
1144
|
+
try {
|
|
1145
|
+
return new Intl.Locale(trimmed).toString().toLowerCase();
|
|
1146
|
+
} catch {
|
|
1147
|
+
return trimmed.toLowerCase();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function createLanguageNormalizer(options) {
|
|
1151
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1152
|
+
const addAliasGroup = (canonical, values) => {
|
|
1153
|
+
const normalizedCanonical = canonicalLanguageTag(canonical);
|
|
1154
|
+
if (!normalizedCanonical) return;
|
|
1155
|
+
aliases.set(normalizedCanonical, normalizedCanonical);
|
|
1156
|
+
for (const value of values) {
|
|
1157
|
+
const normalizedValue = canonicalLanguageTag(value);
|
|
1158
|
+
if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
|
|
1162
|
+
for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
|
|
1163
|
+
addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
|
|
1164
|
+
return (language) => {
|
|
1165
|
+
const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
|
|
1166
|
+
const normalized = canonicalLanguageTag(customValue);
|
|
1167
|
+
return aliases.get(normalized) ?? normalized;
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function voiceLocalePrefix(voiceName) {
|
|
1171
|
+
const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
|
|
1172
|
+
if (!match?.groups) return void 0;
|
|
1173
|
+
const tag = `${match.groups.language}-${match.groups.region}`;
|
|
1174
|
+
return {
|
|
1175
|
+
language: match.groups.language.toLowerCase(),
|
|
1176
|
+
region: match.groups.region.toLowerCase(),
|
|
1177
|
+
tag
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function definitionFromStyleMap(voiceName, styles) {
|
|
1181
|
+
return {
|
|
1182
|
+
name: voiceName,
|
|
1183
|
+
locale: voiceLocalePrefix(voiceName)?.tag ?? "",
|
|
1184
|
+
styles
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeVoiceCatalog(options) {
|
|
1188
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
|
|
1190
|
+
for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
1191
|
+
for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
1192
|
+
for (const definition of options.customVoiceDefinitions ?? [])
|
|
1193
|
+
definitions.set(definition.name.toLowerCase(), definition);
|
|
1194
|
+
for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
|
|
1195
|
+
const key = voiceName.toLowerCase();
|
|
1196
|
+
const current = definitions.get(key);
|
|
1197
|
+
definitions.set(key, {
|
|
1198
|
+
...current ?? definitionFromStyleMap(voiceName, styles),
|
|
1199
|
+
name: current?.name ?? voiceName,
|
|
1200
|
+
styles: styles.map((style) => style.toLowerCase())
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
return definitions;
|
|
1204
|
+
}
|
|
1205
|
+
function diagnosticSeverity(policy) {
|
|
1206
|
+
if (policy === "ignore") return void 0;
|
|
1207
|
+
return policy === "error" ? "error" : "warning";
|
|
1208
|
+
}
|
|
1209
|
+
function languagePart(language) {
|
|
1210
|
+
try {
|
|
1211
|
+
return new Intl.Locale(language).language.toLowerCase();
|
|
1212
|
+
} catch {
|
|
1213
|
+
return language.split("-")[0]?.toLowerCase() ?? "";
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
|
|
1217
|
+
const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
|
|
1218
|
+
if (candidateLanguages.length === 0 || !language.trim()) return void 0;
|
|
1219
|
+
const normalizedLanguage = normalizeLanguage(language);
|
|
1220
|
+
const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
|
|
1221
|
+
if (normalizedCandidates.includes(normalizedLanguage)) return true;
|
|
1222
|
+
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1223
|
+
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1224
|
+
}
|
|
1225
|
+
function canonicalTagName(name) {
|
|
1226
|
+
const normalized = name.toLowerCase();
|
|
1227
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1228
|
+
if (normalized === "sayas") return "say-as";
|
|
1229
|
+
return normalized;
|
|
1230
|
+
}
|
|
1231
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1232
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1233
|
+
return;
|
|
1234
|
+
const tagName = canonicalTagName(token.name);
|
|
1235
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1236
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1237
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1238
|
+
addDiagnostic(
|
|
1239
|
+
diagnostics,
|
|
1240
|
+
source,
|
|
1241
|
+
token.start,
|
|
1242
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1243
|
+
"error",
|
|
1244
|
+
"azure-unsupported-tag-for-voice"
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1249
|
+
const src = attr(token, "src");
|
|
1250
|
+
if (!src) {
|
|
1251
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
let parsed;
|
|
1255
|
+
try {
|
|
1256
|
+
parsed = new URL(src);
|
|
1257
|
+
} catch {
|
|
1258
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1262
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1263
|
+
if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
|
|
1264
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1265
|
+
else if (!options.allowExternalAudio)
|
|
1266
|
+
addDiagnostic(
|
|
1267
|
+
diagnostics,
|
|
1268
|
+
source,
|
|
1269
|
+
token.start,
|
|
1270
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1274
|
+
const name = token.name.toLowerCase();
|
|
1275
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
1276
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
1277
|
+
if (name === "break") {
|
|
1278
|
+
const time = attr(token, "time");
|
|
1279
|
+
const strength = attr(token, "strength");
|
|
1280
|
+
if (!time && !strength)
|
|
1281
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
1282
|
+
if (time && strength)
|
|
1283
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
1284
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
1285
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
1286
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
1287
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
1288
|
+
}
|
|
1289
|
+
if (name === "prosody") {
|
|
1290
|
+
const rate = attr(token, "rate");
|
|
1291
|
+
const pitch = attr(token, "pitch");
|
|
1292
|
+
const volume = attr(token, "volume");
|
|
1293
|
+
if (rate && !isSupportedProsodyRate(rate))
|
|
1294
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
1295
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
1296
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
1297
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
1298
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
1299
|
+
}
|
|
1300
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
1301
|
+
const style = attr(token, "style");
|
|
1302
|
+
if (!style?.trim())
|
|
1303
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
1304
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
1305
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
1306
|
+
addDiagnostic(
|
|
1307
|
+
diagnostics,
|
|
1308
|
+
source,
|
|
1309
|
+
token.start,
|
|
1310
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
1311
|
+
);
|
|
1312
|
+
const role = attr(token, "role");
|
|
1313
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
1314
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
1315
|
+
const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
|
|
1316
|
+
const supportedStyles = definition?.styles;
|
|
1317
|
+
const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
|
|
1318
|
+
if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
|
|
1319
|
+
addDiagnostic(
|
|
1320
|
+
diagnostics,
|
|
1321
|
+
source,
|
|
1322
|
+
token.start,
|
|
1323
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
1324
|
+
severity,
|
|
1325
|
+
"azure-unsupported-style"
|
|
1326
|
+
);
|
|
1327
|
+
if (style && voiceName && !definition && severity)
|
|
1328
|
+
addDiagnostic(
|
|
1329
|
+
diagnostics,
|
|
1330
|
+
source,
|
|
1331
|
+
token.start,
|
|
1332
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
1333
|
+
severity
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (name === "say-as" || name === "sayas") {
|
|
1337
|
+
const interpretAs = attr(token, "interpret-as");
|
|
1338
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
1339
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
1340
|
+
}
|
|
1341
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
1342
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
1343
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
1344
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
1345
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
1346
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
1347
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
1348
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
1349
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
1350
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
1351
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
1352
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
1353
|
+
if (name === "lexicon") {
|
|
1354
|
+
const uri = attr(token, "uri");
|
|
1355
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
1356
|
+
else {
|
|
1357
|
+
try {
|
|
1358
|
+
const parsed = new URL(uri);
|
|
1359
|
+
if (parsed.protocol !== "https:")
|
|
1360
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
1361
|
+
} catch {
|
|
1362
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (name === "mstts:silence") {
|
|
1367
|
+
const type = attr(token, "type");
|
|
1368
|
+
const value = attr(token, "value");
|
|
1369
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
1370
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
1371
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
1372
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
1373
|
+
}
|
|
1374
|
+
if (name === "mstts:audioduration") {
|
|
1375
|
+
const value = attr(token, "value");
|
|
1376
|
+
if (!value || !isValidAzureAudioDuration(value))
|
|
1377
|
+
addDiagnostic(
|
|
1378
|
+
diagnostics,
|
|
1379
|
+
source,
|
|
1380
|
+
token.start,
|
|
1381
|
+
'<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
|
|
1382
|
+
);
|
|
1383
|
+
if (!token.selfClosing)
|
|
1384
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
|
|
1385
|
+
}
|
|
1386
|
+
if (name === "mstts:viseme") {
|
|
1387
|
+
const type = attr(token, "type");
|
|
1388
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
1389
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1390
|
+
}
|
|
1391
|
+
if (name === "audio") {
|
|
1392
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1393
|
+
}
|
|
1394
|
+
if (name === "mstts:turn") {
|
|
1395
|
+
if (!attr(token, "voice")?.trim())
|
|
1396
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
|
|
1397
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
1398
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
1399
|
+
}
|
|
1400
|
+
if (name === "mstts:backgroundaudio") {
|
|
1401
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
1402
|
+
const volume = attr(token, "volume");
|
|
1403
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
|
|
1404
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
1405
|
+
for (const [attribute, value] of [
|
|
1406
|
+
["fadein", attr(token, "fadein")],
|
|
1407
|
+
["fadeout", attr(token, "fadeout")]
|
|
1408
|
+
]) {
|
|
1409
|
+
if (value && !isValidAzureAudioDuration(value))
|
|
1410
|
+
addDiagnostic(
|
|
1411
|
+
diagnostics,
|
|
1412
|
+
source,
|
|
1413
|
+
token.start,
|
|
1414
|
+
`<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (!token.selfClosing)
|
|
1418
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
1422
|
+
const diagnostics = [];
|
|
1423
|
+
if (typeof ssml !== "string") {
|
|
1424
|
+
return [
|
|
1425
|
+
{
|
|
1426
|
+
line: 1,
|
|
1427
|
+
column: 1,
|
|
1428
|
+
message: "SSML input must be a string",
|
|
1429
|
+
severity: "error",
|
|
1430
|
+
source: "ssml-static-validator"
|
|
1431
|
+
}
|
|
1432
|
+
];
|
|
1433
|
+
}
|
|
1434
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
1435
|
+
if (ssml.length > maxLength)
|
|
1436
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
1437
|
+
try {
|
|
1438
|
+
parseSsml(ssml);
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
1441
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
1442
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
1443
|
+
return diagnostics;
|
|
1444
|
+
}
|
|
1445
|
+
const tokens = tokenizeElements(ssml);
|
|
1446
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
1447
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
1448
|
+
if (!speak || voices.length === 0)
|
|
1449
|
+
addDiagnostic(
|
|
1450
|
+
diagnostics,
|
|
1451
|
+
ssml,
|
|
1452
|
+
speak?.start ?? 0,
|
|
1453
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
1454
|
+
);
|
|
1455
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
1456
|
+
const voiceCatalog = normalizeVoiceCatalog(options);
|
|
1457
|
+
const normalizeLanguage = createLanguageNormalizer(options);
|
|
1458
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
1459
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
1460
|
+
for (const token of voicesToValidate) {
|
|
1461
|
+
const name = attr(token, "name")?.trim();
|
|
1462
|
+
const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
|
|
1463
|
+
const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
|
|
1464
|
+
if (name && !definition && policySeverity)
|
|
1465
|
+
addDiagnostic(
|
|
1466
|
+
diagnostics,
|
|
1467
|
+
ssml,
|
|
1468
|
+
token.start,
|
|
1469
|
+
`Unknown voice "${name}" is not registered in the voice catalog.`,
|
|
1470
|
+
policySeverity,
|
|
1471
|
+
"azure-unknown-voice"
|
|
1472
|
+
);
|
|
1473
|
+
if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
|
|
1474
|
+
addDiagnostic(
|
|
1475
|
+
diagnostics,
|
|
1476
|
+
ssml,
|
|
1477
|
+
token.start,
|
|
1478
|
+
`Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
|
|
1479
|
+
"warning",
|
|
1480
|
+
"azure-locale-mismatch"
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
for (const token of tokens) {
|
|
1484
|
+
const tokenName = token.name.toLowerCase();
|
|
1485
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1486
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
1487
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
1488
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
1489
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
1490
|
+
addDiagnostic(
|
|
1491
|
+
diagnostics,
|
|
1492
|
+
ssml,
|
|
1493
|
+
token.start,
|
|
1494
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
1495
|
+
"error",
|
|
1496
|
+
"azure-unsupported-model-for-voice"
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return diagnostics;
|
|
1501
|
+
}
|
|
1502
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
1503
|
+
apiVersion: "2025-10-01",
|
|
1504
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
1505
|
+
regions: [],
|
|
1506
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
1507
|
+
};
|
|
813
1508
|
|
|
814
1509
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
815
1510
|
function getDocumentChildren(document2) {
|
|
@@ -1948,6 +2643,11 @@ var SSML_ATTRIBUTE_PRESETS = {
|
|
|
1948
2643
|
"mstts:audioduration": {
|
|
1949
2644
|
value: AUDIO_DURATION_PRESETS
|
|
1950
2645
|
},
|
|
2646
|
+
"mstts:backgroundaudio": {
|
|
2647
|
+
volume: PROSODY_VOLUME_PRESETS,
|
|
2648
|
+
fadein: AUDIO_DURATION_PRESETS,
|
|
2649
|
+
fadeout: AUDIO_DURATION_PRESETS
|
|
2650
|
+
},
|
|
1951
2651
|
silence: {
|
|
1952
2652
|
type: SILENCE_TYPE_PRESETS,
|
|
1953
2653
|
value: SILENCE_VALUE_PRESETS
|
|
@@ -2284,7 +2984,7 @@ var SILENCE_VALUE_DESCRIPTIONS = {
|
|
|
2284
2984
|
};
|
|
2285
2985
|
|
|
2286
2986
|
// packages/ssml-editor-react/src/ssmlContext.ts
|
|
2287
|
-
function
|
|
2987
|
+
function findTagEnd3(source, start, limit) {
|
|
2288
2988
|
let quote;
|
|
2289
2989
|
for (let index = start + 1; index < limit; index += 1) {
|
|
2290
2990
|
const character = source[index];
|
|
@@ -2334,7 +3034,7 @@ function findActiveSsmlTags(source, offset) {
|
|
|
2334
3034
|
index = end;
|
|
2335
3035
|
continue;
|
|
2336
3036
|
}
|
|
2337
|
-
const tagEnd =
|
|
3037
|
+
const tagEnd = findTagEnd3(source, tagStart, source.length);
|
|
2338
3038
|
const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
|
|
2339
3039
|
const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
2340
3040
|
const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
|
|
@@ -2377,7 +3077,7 @@ function findSsmlVoiceContext(source, offset) {
|
|
|
2377
3077
|
index = end === -1 || end + 2 > limit ? limit : end + 2;
|
|
2378
3078
|
continue;
|
|
2379
3079
|
}
|
|
2380
|
-
const tagEnd =
|
|
3080
|
+
const tagEnd = findTagEnd3(source, tagStart, limit);
|
|
2381
3081
|
if (tagEnd === -1) {
|
|
2382
3082
|
break;
|
|
2383
3083
|
}
|
|
@@ -2439,6 +3139,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
2439
3139
|
label: "mstts:audioduration",
|
|
2440
3140
|
insertText: '<mstts:audioduration value="10s" />'
|
|
2441
3141
|
},
|
|
3142
|
+
{
|
|
3143
|
+
label: "mstts:dialog",
|
|
3144
|
+
insertText: `<mstts:dialog>
|
|
3145
|
+
<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
|
|
3146
|
+
</mstts:dialog>`
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
label: "mstts:turn",
|
|
3150
|
+
insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
|
|
3151
|
+
},
|
|
3152
|
+
{
|
|
3153
|
+
label: "mstts:backgroundaudio",
|
|
3154
|
+
insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
label: "mstts:ttsembedding",
|
|
3158
|
+
insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
|
|
3159
|
+
},
|
|
2442
3160
|
{
|
|
2443
3161
|
label: "sub",
|
|
2444
3162
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -3184,6 +3902,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
3184
3902
|
example: "10s"
|
|
3185
3903
|
}
|
|
3186
3904
|
]
|
|
3905
|
+
},
|
|
3906
|
+
{
|
|
3907
|
+
name: "mstts:dialog",
|
|
3908
|
+
description: "Groups multiple Azure dialog turns that can use different voices.",
|
|
3909
|
+
parameters: []
|
|
3910
|
+
},
|
|
3911
|
+
{
|
|
3912
|
+
name: "mstts:turn",
|
|
3913
|
+
description: "Adds one dialog turn using the required Azure voice name.",
|
|
3914
|
+
parameters: [
|
|
3915
|
+
{
|
|
3916
|
+
name: "voice",
|
|
3917
|
+
description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
|
|
3918
|
+
example: "en-US-JennyNeural"
|
|
3919
|
+
}
|
|
3920
|
+
]
|
|
3921
|
+
},
|
|
3922
|
+
{
|
|
3923
|
+
name: "mstts:backgroundaudio",
|
|
3924
|
+
description: "Plays background audio while speech is synthesized.",
|
|
3925
|
+
parameters: [
|
|
3926
|
+
{
|
|
3927
|
+
name: "src",
|
|
3928
|
+
description: "An absolute HTTP(S) URL for the background audio file.",
|
|
3929
|
+
example: "https://example.com/music.mp3"
|
|
3930
|
+
},
|
|
3931
|
+
{
|
|
3932
|
+
name: "volume",
|
|
3933
|
+
description: "The background audio volume, for example `-3dB` or `medium`.",
|
|
3934
|
+
example: "-3dB"
|
|
3935
|
+
},
|
|
3936
|
+
{
|
|
3937
|
+
name: "fadein",
|
|
3938
|
+
description: "The fade-in duration, for example `1s`.",
|
|
3939
|
+
example: "1s"
|
|
3940
|
+
},
|
|
3941
|
+
{
|
|
3942
|
+
name: "fadeout",
|
|
3943
|
+
description: "The fade-out duration, for example `500ms`.",
|
|
3944
|
+
example: "500ms"
|
|
3945
|
+
}
|
|
3946
|
+
]
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
name: "mstts:ttsembedding",
|
|
3950
|
+
description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
|
|
3951
|
+
parameters: []
|
|
3952
|
+
},
|
|
3953
|
+
{
|
|
3954
|
+
name: "mstts:embedding",
|
|
3955
|
+
description: "Specifies embedding metadata for custom voice scenarios.",
|
|
3956
|
+
parameters: []
|
|
3957
|
+
},
|
|
3958
|
+
{
|
|
3959
|
+
name: "mstts:voiceconversion",
|
|
3960
|
+
description: "Specifies voice conversion metadata for custom voice scenarios.",
|
|
3961
|
+
parameters: []
|
|
3187
3962
|
}
|
|
3188
3963
|
];
|
|
3189
3964
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -3242,7 +4017,7 @@ function toRange(source, token) {
|
|
|
3242
4017
|
function containsOffset(token, offset) {
|
|
3243
4018
|
return offset >= token.start && offset < token.end;
|
|
3244
4019
|
}
|
|
3245
|
-
function
|
|
4020
|
+
function findTagEnd4(source, start) {
|
|
3246
4021
|
let quote;
|
|
3247
4022
|
for (let index = start; index < source.length; index += 1) {
|
|
3248
4023
|
const character = source[index];
|
|
@@ -3373,7 +4148,7 @@ function findTagAtOffset(source, offset) {
|
|
|
3373
4148
|
searchStart = tokenEnd2;
|
|
3374
4149
|
continue;
|
|
3375
4150
|
}
|
|
3376
|
-
const tagEnd =
|
|
4151
|
+
const tagEnd = findTagEnd4(source, start + 1);
|
|
3377
4152
|
const contentEnd = tagEnd ?? source.length;
|
|
3378
4153
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
3379
4154
|
if (offset < tokenEnd) {
|
|
@@ -3989,6 +4764,60 @@ section[data-ssml-editor] {
|
|
|
3989
4764
|
border-radius: 0.25rem;
|
|
3990
4765
|
overflow: visible;
|
|
3991
4766
|
}
|
|
4767
|
+
[data-ssml-editor] .ssml-editor-visual {
|
|
4768
|
+
display: grid;
|
|
4769
|
+
gap: 0.75rem;
|
|
4770
|
+
min-height: 8rem;
|
|
4771
|
+
padding: 0.75rem;
|
|
4772
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4773
|
+
border-radius: 0.25rem;
|
|
4774
|
+
}
|
|
4775
|
+
[data-ssml-editor] .ssml-editor-visual-layout {
|
|
4776
|
+
display: grid;
|
|
4777
|
+
grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
|
|
4778
|
+
gap: 1rem;
|
|
4779
|
+
}
|
|
4780
|
+
[data-ssml-editor] .ssml-editor-visual-tree ul {
|
|
4781
|
+
margin: 0;
|
|
4782
|
+
padding-left: 1.25rem;
|
|
4783
|
+
}
|
|
4784
|
+
[data-ssml-editor] .ssml-editor-visual button {
|
|
4785
|
+
padding: 0.35rem 0.5rem;
|
|
4786
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4787
|
+
border-radius: 0.25rem;
|
|
4788
|
+
color: var(--ssml-editor-color);
|
|
4789
|
+
background: var(--ssml-editor-control-bg);
|
|
4790
|
+
cursor: pointer;
|
|
4791
|
+
}
|
|
4792
|
+
[data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
|
|
4793
|
+
border-color: var(--ssml-editor-active-border);
|
|
4794
|
+
background: var(--ssml-editor-active-bg);
|
|
4795
|
+
}
|
|
4796
|
+
[data-ssml-editor] .ssml-editor-visual textarea {
|
|
4797
|
+
box-sizing: border-box;
|
|
4798
|
+
width: 100%;
|
|
4799
|
+
min-height: 6rem;
|
|
4800
|
+
padding: 0.5rem;
|
|
4801
|
+
color: var(--ssml-editor-color);
|
|
4802
|
+
background: var(--ssml-editor-control-bg);
|
|
4803
|
+
border: 1px solid var(--ssml-editor-control-border);
|
|
4804
|
+
border-radius: 0.25rem;
|
|
4805
|
+
font: inherit;
|
|
4806
|
+
}
|
|
4807
|
+
[data-ssml-editor] .ssml-editor-visual-actions,
|
|
4808
|
+
[data-ssml-editor] .ssml-editor-visual-breadcrumb {
|
|
4809
|
+
display: flex;
|
|
4810
|
+
flex-wrap: wrap;
|
|
4811
|
+
gap: 0.4rem;
|
|
4812
|
+
align-items: center;
|
|
4813
|
+
}
|
|
4814
|
+
[data-ssml-editor] .ssml-editor-visual-errors {
|
|
4815
|
+
padding: 0.5rem;
|
|
4816
|
+
color: #b91c1c;
|
|
4817
|
+
background: #fef2f2;
|
|
4818
|
+
border: 1px solid #b91c1c;
|
|
4819
|
+
border-radius: 0.25rem;
|
|
4820
|
+
}
|
|
3992
4821
|
`.trim();
|
|
3993
4822
|
function injectStyles() {
|
|
3994
4823
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
|
|
@@ -4002,6 +4831,55 @@ function injectStyles() {
|
|
|
4002
4831
|
function isDarkTheme(theme) {
|
|
4003
4832
|
return theme === "vs-dark" || theme.toLowerCase().includes("dark");
|
|
4004
4833
|
}
|
|
4834
|
+
function isElement(node) {
|
|
4835
|
+
return typeof node !== "string" && node.type !== "text";
|
|
4836
|
+
}
|
|
4837
|
+
function visualElementName(element) {
|
|
4838
|
+
return element.type === "custom" || element.type === "element" ? element.name : element.type;
|
|
4839
|
+
}
|
|
4840
|
+
function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
|
|
4841
|
+
return nodes.flatMap((node, index) => {
|
|
4842
|
+
const currentPath = [...path, index];
|
|
4843
|
+
if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
|
|
4844
|
+
if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
|
|
4845
|
+
return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
function updateVisualNodes(nodes, path, update) {
|
|
4849
|
+
if (path.length === 0) return nodes;
|
|
4850
|
+
const [index, ...rest] = path;
|
|
4851
|
+
return nodes.flatMap((node, nodeIndex) => {
|
|
4852
|
+
if (nodeIndex !== index) return [node];
|
|
4853
|
+
if (rest.length === 0) {
|
|
4854
|
+
const next = update(node);
|
|
4855
|
+
return Array.isArray(next) ? next : [next];
|
|
4856
|
+
}
|
|
4857
|
+
if (!isElement(node)) return [node];
|
|
4858
|
+
return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
|
|
4859
|
+
});
|
|
4860
|
+
}
|
|
4861
|
+
function updateVisualText(document2, path, value) {
|
|
4862
|
+
return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
|
|
4863
|
+
}
|
|
4864
|
+
function wrapVisualText(document2, path, start, end, type, attributes) {
|
|
4865
|
+
return {
|
|
4866
|
+
...document2,
|
|
4867
|
+
children: updateVisualNodes(document2.children ?? [], path, (node) => {
|
|
4868
|
+
const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
|
|
4869
|
+
if (!value || start === end) return node;
|
|
4870
|
+
if (type === "break") {
|
|
4871
|
+
return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
|
|
4872
|
+
(part) => typeof part === "string" ? part.length > 0 : true
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4875
|
+
const selected = value.slice(start, end);
|
|
4876
|
+
const wrapper = { type, attributes, children: [selected] };
|
|
4877
|
+
return [value.slice(0, start), wrapper, value.slice(end)].filter(
|
|
4878
|
+
(part) => typeof part === "string" ? part.length > 0 : true
|
|
4879
|
+
);
|
|
4880
|
+
})
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4005
4883
|
function getMenuPosition(trigger, menu) {
|
|
4006
4884
|
const bounds = trigger.getBoundingClientRect();
|
|
4007
4885
|
const margin = 8;
|
|
@@ -4023,6 +4901,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4023
4901
|
this.toolbarActions = null;
|
|
4024
4902
|
this.display = null;
|
|
4025
4903
|
this.editorContainer = null;
|
|
4904
|
+
this.visualContainer = null;
|
|
4026
4905
|
this.helpPanel = null;
|
|
4027
4906
|
this.openMenu = null;
|
|
4028
4907
|
this.openMenuTrigger = null;
|
|
@@ -4036,6 +4915,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4036
4915
|
this.documentState = null;
|
|
4037
4916
|
this.decorationsVisible = false;
|
|
4038
4917
|
this.helpOpen = false;
|
|
4918
|
+
this.visualSelectedPath = null;
|
|
4919
|
+
this.visualSelection = { start: 0, end: 0 };
|
|
4039
4920
|
this.handleDocumentPointerDown = (event) => {
|
|
4040
4921
|
const target = event.target;
|
|
4041
4922
|
if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
|
|
@@ -4077,6 +4958,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4077
4958
|
set locale(locale) {
|
|
4078
4959
|
this.setAttribute("locale", locale);
|
|
4079
4960
|
}
|
|
4961
|
+
get editMode() {
|
|
4962
|
+
return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
|
|
4963
|
+
}
|
|
4964
|
+
set editMode(mode) {
|
|
4965
|
+
this.setAttribute("edit-mode", mode);
|
|
4966
|
+
}
|
|
4080
4967
|
prepareDocument(value) {
|
|
4081
4968
|
try {
|
|
4082
4969
|
this.documentState = parseSsml(value);
|
|
@@ -4119,6 +5006,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4119
5006
|
}
|
|
4120
5007
|
}
|
|
4121
5008
|
}
|
|
5009
|
+
this.renderVisualEditor();
|
|
4122
5010
|
return;
|
|
4123
5011
|
}
|
|
4124
5012
|
if (name === "theme" && this.monaco) {
|
|
@@ -4136,6 +5024,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4136
5024
|
this.renderHelp();
|
|
4137
5025
|
return;
|
|
4138
5026
|
}
|
|
5027
|
+
if (name === "edit-mode") {
|
|
5028
|
+
this.updateEditMode();
|
|
5029
|
+
}
|
|
4139
5030
|
if (name === "show-decorations") {
|
|
4140
5031
|
this.decorationsVisible = newValue !== null;
|
|
4141
5032
|
this.updateDecorations();
|
|
@@ -4160,7 +5051,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4160
5051
|
display.dataset.ssmlEditorDisplay = "";
|
|
4161
5052
|
const editorContainer = document.createElement("div");
|
|
4162
5053
|
editorContainer.className = "ssml-editor-editor";
|
|
4163
|
-
|
|
5054
|
+
const visualContainer = document.createElement("div");
|
|
5055
|
+
visualContainer.className = "ssml-editor-visual";
|
|
5056
|
+
display.append(editorContainer, visualContainer);
|
|
4164
5057
|
root.append(toolbar, display);
|
|
4165
5058
|
this.replaceChildren(root);
|
|
4166
5059
|
this.root = root;
|
|
@@ -4168,8 +5061,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4168
5061
|
this.toolbarActions = toolbarActions;
|
|
4169
5062
|
this.display = display;
|
|
4170
5063
|
this.editorContainer = editorContainer;
|
|
5064
|
+
this.visualContainer = visualContainer;
|
|
4171
5065
|
this.renderToolbar();
|
|
4172
5066
|
this.renderHelp();
|
|
5067
|
+
this.updateEditMode();
|
|
5068
|
+
this.renderVisualEditor();
|
|
4173
5069
|
}
|
|
4174
5070
|
renderToolbar() {
|
|
4175
5071
|
const toolbar = this.toolbar;
|
|
@@ -4213,10 +5109,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4213
5109
|
for (const id of toolbarIds) {
|
|
4214
5110
|
const group = groupByButtonId.get(id);
|
|
4215
5111
|
if (previousGroup !== void 0 && group !== previousGroup) {
|
|
4216
|
-
const
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
toolbarActions.append(
|
|
5112
|
+
const separator2 = document.createElement("span");
|
|
5113
|
+
separator2.className = "ssml-editor-toolbar-separator";
|
|
5114
|
+
separator2.setAttribute("aria-hidden", "true");
|
|
5115
|
+
toolbarActions.append(separator2);
|
|
4220
5116
|
}
|
|
4221
5117
|
previousGroup = group;
|
|
4222
5118
|
const insertion = insertionById.get(id);
|
|
@@ -4228,8 +5124,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4228
5124
|
toolbarActions.append(this.createActionButton(id));
|
|
4229
5125
|
}
|
|
4230
5126
|
}
|
|
5127
|
+
const separator = document.createElement("span");
|
|
5128
|
+
separator.className = "ssml-editor-toolbar-separator";
|
|
5129
|
+
separator.setAttribute("aria-hidden", "true");
|
|
5130
|
+
toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
|
|
4231
5131
|
this.updateActiveButtons();
|
|
4232
5132
|
}
|
|
5133
|
+
createModeButton(mode, label) {
|
|
5134
|
+
const button = document.createElement("button");
|
|
5135
|
+
button.type = "button";
|
|
5136
|
+
button.className = "ssml-editor-toolbar-button";
|
|
5137
|
+
button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
|
|
5138
|
+
button.setAttribute("aria-pressed", String(this.editMode === mode));
|
|
5139
|
+
button.textContent = label;
|
|
5140
|
+
button.addEventListener("click", () => {
|
|
5141
|
+
this.editMode = mode;
|
|
5142
|
+
});
|
|
5143
|
+
return button;
|
|
5144
|
+
}
|
|
4233
5145
|
createActionButton(id) {
|
|
4234
5146
|
const copy = EDITOR_COPY[this.locale];
|
|
4235
5147
|
const labels = {
|
|
@@ -4609,6 +5521,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4609
5521
|
this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
|
|
4610
5522
|
}
|
|
4611
5523
|
}
|
|
5524
|
+
updateEditMode() {
|
|
5525
|
+
if (this.editorContainer && this.visualContainer) {
|
|
5526
|
+
const visual = this.editMode === "visual";
|
|
5527
|
+
this.editorContainer.hidden = visual;
|
|
5528
|
+
this.visualContainer.hidden = !visual;
|
|
5529
|
+
}
|
|
5530
|
+
for (const mode of ["visual", "code"]) {
|
|
5531
|
+
const button = this.toolbarActions?.querySelector(
|
|
5532
|
+
`[data-ssml-editor-button="edit-mode-${mode}"]`
|
|
5533
|
+
);
|
|
5534
|
+
button?.setAttribute("aria-pressed", String(this.editMode === mode));
|
|
5535
|
+
button?.toggleAttribute("data-active", this.editMode === mode);
|
|
5536
|
+
}
|
|
5537
|
+
this.renderVisualEditor();
|
|
5538
|
+
}
|
|
5539
|
+
renderVisualEditor() {
|
|
5540
|
+
const container = this.visualContainer;
|
|
5541
|
+
if (!container) return;
|
|
5542
|
+
container.replaceChildren();
|
|
5543
|
+
if (!this.documentState) {
|
|
5544
|
+
const error = document.createElement("p");
|
|
5545
|
+
error.className = "ssml-editor-visual-errors";
|
|
5546
|
+
error.textContent = "SSML syntax must be valid before visual editing is available.";
|
|
5547
|
+
container.append(error);
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
5550
|
+
const documentState = this.documentState;
|
|
5551
|
+
const leaves = collectVisualTextLeaves(documentState.children ?? []);
|
|
5552
|
+
const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
|
|
5553
|
+
const breadcrumb = document.createElement("div");
|
|
5554
|
+
breadcrumb.className = "ssml-editor-visual-breadcrumb";
|
|
5555
|
+
breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
|
|
5556
|
+
const clear = document.createElement("button");
|
|
5557
|
+
clear.type = "button";
|
|
5558
|
+
clear.textContent = "Clear parent";
|
|
5559
|
+
clear.disabled = !this.visualSelectedPath;
|
|
5560
|
+
clear.addEventListener("click", () => {
|
|
5561
|
+
this.visualSelectedPath = null;
|
|
5562
|
+
this.renderVisualEditor();
|
|
5563
|
+
});
|
|
5564
|
+
breadcrumb.append(clear);
|
|
5565
|
+
container.append(breadcrumb);
|
|
5566
|
+
const diagnostics = validateAzureSsml(buildSsml(documentState));
|
|
5567
|
+
if (diagnostics.length > 0) {
|
|
5568
|
+
const errors = document.createElement("div");
|
|
5569
|
+
errors.className = "ssml-editor-visual-errors";
|
|
5570
|
+
errors.setAttribute("role", "alert");
|
|
5571
|
+
for (const diagnostic of diagnostics) {
|
|
5572
|
+
const message = document.createElement("div");
|
|
5573
|
+
message.textContent = diagnostic.message;
|
|
5574
|
+
errors.append(message);
|
|
5575
|
+
}
|
|
5576
|
+
container.append(errors);
|
|
5577
|
+
}
|
|
5578
|
+
const layout = document.createElement("div");
|
|
5579
|
+
layout.className = "ssml-editor-visual-layout";
|
|
5580
|
+
const tree = document.createElement("nav");
|
|
5581
|
+
tree.className = "ssml-editor-visual-tree";
|
|
5582
|
+
tree.setAttribute("aria-label", "SSML structure tree");
|
|
5583
|
+
tree.append(document.createTextNode("Structure"));
|
|
5584
|
+
const treeList = document.createElement("ul");
|
|
5585
|
+
const renderTree = (nodes, parent, parentPath = []) => {
|
|
5586
|
+
nodes.forEach((node, index) => {
|
|
5587
|
+
if (!isElement(node)) return;
|
|
5588
|
+
const path = [...parentPath, index];
|
|
5589
|
+
const item = document.createElement("li");
|
|
5590
|
+
const button = document.createElement("button");
|
|
5591
|
+
button.type = "button";
|
|
5592
|
+
button.textContent = `<${visualElementName(node)}>`;
|
|
5593
|
+
button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
|
|
5594
|
+
button.addEventListener("click", () => {
|
|
5595
|
+
this.visualSelectedPath = path;
|
|
5596
|
+
this.renderVisualEditor();
|
|
5597
|
+
});
|
|
5598
|
+
item.append(button);
|
|
5599
|
+
if ((node.children ?? []).some(isElement)) {
|
|
5600
|
+
const childList = document.createElement("ul");
|
|
5601
|
+
renderTree(node.children ?? [], childList, path);
|
|
5602
|
+
item.append(childList);
|
|
5603
|
+
}
|
|
5604
|
+
parent.append(item);
|
|
5605
|
+
});
|
|
5606
|
+
};
|
|
5607
|
+
renderTree(documentState.children ?? [], treeList);
|
|
5608
|
+
tree.append(treeList);
|
|
5609
|
+
layout.append(tree);
|
|
5610
|
+
const form = document.createElement("div");
|
|
5611
|
+
form.className = "ssml-editor-visual-form";
|
|
5612
|
+
if (selectedLeaf) {
|
|
5613
|
+
const label = document.createElement("label");
|
|
5614
|
+
label.append(document.createTextNode("Text"));
|
|
5615
|
+
const textarea = document.createElement("textarea");
|
|
5616
|
+
textarea.value = selectedLeaf.value;
|
|
5617
|
+
textarea.readOnly = this.readonly;
|
|
5618
|
+
textarea.addEventListener("select", () => {
|
|
5619
|
+
this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
|
|
5620
|
+
});
|
|
5621
|
+
textarea.addEventListener("input", () => {
|
|
5622
|
+
if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
|
|
5623
|
+
});
|
|
5624
|
+
label.append(textarea);
|
|
5625
|
+
form.append(label);
|
|
5626
|
+
const actions = document.createElement("div");
|
|
5627
|
+
actions.className = "ssml-editor-visual-actions";
|
|
5628
|
+
const wrappers = [
|
|
5629
|
+
["Rate", "prosody", { rate: "slow" }],
|
|
5630
|
+
["Pitch", "prosody", { pitch: "high" }],
|
|
5631
|
+
["Emotion", "mstts:express-as", { style: "cheerful" }],
|
|
5632
|
+
["Pause", "break", { time: "500ms" }],
|
|
5633
|
+
["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
|
|
5634
|
+
];
|
|
5635
|
+
for (const [labelText, type, attributes] of wrappers) {
|
|
5636
|
+
const button = document.createElement("button");
|
|
5637
|
+
button.type = "button";
|
|
5638
|
+
button.textContent = labelText;
|
|
5639
|
+
button.disabled = this.readonly;
|
|
5640
|
+
button.addEventListener("click", () => {
|
|
5641
|
+
const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
|
|
5642
|
+
const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
|
|
5643
|
+
this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
|
|
5644
|
+
this.renderVisualEditor();
|
|
5645
|
+
});
|
|
5646
|
+
actions.append(button);
|
|
5647
|
+
}
|
|
5648
|
+
const preview = document.createElement("button");
|
|
5649
|
+
preview.type = "button";
|
|
5650
|
+
preview.textContent = "Preview selection";
|
|
5651
|
+
preview.addEventListener("click", () => {
|
|
5652
|
+
this.dispatchEvent(
|
|
5653
|
+
new CustomEvent("preview-selection", {
|
|
5654
|
+
detail: { ssml: buildSsml({ ...documentState, children: [selectedLeaf.value] }) },
|
|
5655
|
+
bubbles: true,
|
|
5656
|
+
composed: true
|
|
5657
|
+
})
|
|
5658
|
+
);
|
|
5659
|
+
});
|
|
5660
|
+
actions.append(preview);
|
|
5661
|
+
form.append(actions);
|
|
5662
|
+
} else {
|
|
5663
|
+
form.textContent = "Select an element or text node to edit it.";
|
|
5664
|
+
}
|
|
5665
|
+
layout.append(form);
|
|
5666
|
+
container.append(layout);
|
|
5667
|
+
}
|
|
4612
5668
|
updateActiveButtons() {
|
|
4613
5669
|
const editor = this.editor;
|
|
4614
5670
|
const model = this.model;
|
|
@@ -4636,6 +5692,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
|
|
|
4636
5692
|
return;
|
|
4637
5693
|
}
|
|
4638
5694
|
const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
|
|
5695
|
+
this.renderVisualEditor();
|
|
4639
5696
|
const editor = monacoModule.editor.create(container, {
|
|
4640
5697
|
model,
|
|
4641
5698
|
theme: this.theme,
|
|
@@ -4734,7 +5791,8 @@ SsmlEditorElement.observedAttributes = [
|
|
|
4734
5791
|
"locale",
|
|
4735
5792
|
"show-toolbar",
|
|
4736
5793
|
"show-toolbar-labels",
|
|
4737
|
-
"show-decorations"
|
|
5794
|
+
"show-decorations",
|
|
5795
|
+
"edit-mode"
|
|
4738
5796
|
];
|
|
4739
5797
|
|
|
4740
5798
|
// packages/ssml-editor-elements/src/index.ts
|