ssml-builder-js 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/elements.js CHANGED
@@ -74,7 +74,14 @@ var SSML_TAGS = {
74
74
  MSTTS_SILENCE: "mstts:silence",
75
75
  SILENCE: "silence",
76
76
  MSTTS_VISEME: "mstts:viseme",
77
- VISEME: "viseme"
77
+ VISEME: "viseme",
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"
78
85
  };
79
86
  var SSML_ATTRS = {
80
87
  VERSION: "version",
@@ -83,6 +90,7 @@ var SSML_ATTRS = {
83
90
  LANG: "lang",
84
91
  MSTTS_XMLNS: "xmlns:mstts",
85
92
  NAME: "name",
93
+ VOICE: "voice",
86
94
  EFFECT: "effect",
87
95
  RATE: "rate",
88
96
  PITCH: "pitch",
@@ -114,7 +122,9 @@ var SSML_ATTRS = {
114
122
  MARK: "mark",
115
123
  URI: "uri",
116
124
  TYPE: "type",
117
- VALUE: "value"
125
+ VALUE: "value",
126
+ FADE_IN: "fadein",
127
+ FADE_OUT: "fadeout"
118
128
  };
119
129
  function escapeText(value) {
120
130
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -201,6 +211,23 @@ function getAttributes(element) {
201
211
  case SSML_TAGS.VISEME:
202
212
  addAttribute(attributes, SSML_ATTRS.TYPE, element.typeValue ?? element.visemeType);
203
213
  break;
214
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
215
+ addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
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;
204
231
  case SSML_TAGS.PARAGRAPH:
205
232
  case SSML_TAGS.SENTENCE:
206
233
  case SSML_TAGS.WORD:
@@ -225,6 +252,8 @@ function getTagName(element) {
225
252
  case SSML_TAGS.VISEME:
226
253
  case SSML_TAGS.MSTTS_VISEME:
227
254
  return SSML_TAGS.MSTTS_VISEME;
255
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
256
+ return SSML_TAGS.MSTTS_AUDIO_DURATION;
228
257
  case "element":
229
258
  case "custom":
230
259
  return element.name;
@@ -757,6 +786,46 @@ function convertElement(node) {
757
786
  if (typeValue !== void 0) element.typeValue = typeValue;
758
787
  return finishElement(element, node, attributes);
759
788
  }
789
+ case SSML_TAGS.MSTTS_AUDIO_DURATION: {
790
+ const element = { type: SSML_TAGS.MSTTS_AUDIO_DURATION };
791
+ const value = readAttribute(attributes, SSML_ATTRS.VALUE);
792
+ if (value !== void 0) element.value = value;
793
+ return finishElement(element, node, attributes);
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
+ }
760
829
  default: {
761
830
  const element = {
762
831
  name: node.name,
@@ -798,6 +867,644 @@ function parseSsml(xmlString) {
798
867
  }
799
868
  return document2;
800
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) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[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
+ };
801
1508
 
802
1509
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
803
1510
  function getDocumentChildren(document2) {
@@ -858,6 +1565,7 @@ var INTRINSICALLY_EMPTY_ELEMENTS = /* @__PURE__ */ new Set([
858
1565
  "lexicon",
859
1566
  "mark",
860
1567
  "mstts:silence",
1568
+ "mstts:audioduration",
861
1569
  "mstts:viseme",
862
1570
  "silence",
863
1571
  "viseme"
@@ -1516,6 +2224,8 @@ function updateEditableText(document2, value) {
1516
2224
  // packages/ssml-editor-react/src/constants/ssmlPresets.ts
1517
2225
  var ssmlPresets_exports = {};
1518
2226
  __export(ssmlPresets_exports, {
2227
+ AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
2228
+ AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
1519
2229
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
1520
2230
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
1521
2231
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -1548,6 +2258,103 @@ __export(ssmlPresets_exports, {
1548
2258
  getExpressAsStyleCategory: () => getExpressAsStyleCategory,
1549
2259
  resolveExpressAsStyles: () => resolveExpressAsStyles
1550
2260
  });
2261
+
2262
+ // packages/ssml-editor-react/src/constants/azureVoiceStyleMap.generated.ts
2263
+ var AZURE_VOICE_STYLE_MAP = {
2264
+ "de-DE-ConradNeural": ["cheerful", "sad"],
2265
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
2266
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
2267
+ "en-US-GuyNeural": [
2268
+ "angry",
2269
+ "cheerful",
2270
+ "excited",
2271
+ "friendly",
2272
+ "hopeful",
2273
+ "newscast",
2274
+ "sad",
2275
+ "shouting",
2276
+ "terrified",
2277
+ "unfriendly",
2278
+ "whispering"
2279
+ ],
2280
+ "en-US-JennyMultilingualNeural": [
2281
+ "cheerful",
2282
+ "empathetic",
2283
+ "excited",
2284
+ "friendly",
2285
+ "hopeful",
2286
+ "sad",
2287
+ "shouting",
2288
+ "terrified",
2289
+ "unfriendly",
2290
+ "whispering"
2291
+ ],
2292
+ "en-US-JennyNeural": [
2293
+ "assistant",
2294
+ "chat",
2295
+ "customerservice",
2296
+ "newscast",
2297
+ "cheerful",
2298
+ "empathetic",
2299
+ "excited",
2300
+ "friendly",
2301
+ "hopeful",
2302
+ "sad",
2303
+ "shouting",
2304
+ "terrified",
2305
+ "unfriendly",
2306
+ "whispering"
2307
+ ],
2308
+ "es-ES-ElviraNeural": [],
2309
+ "fil-PH-AngeloNeural": [],
2310
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
2311
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
2312
+ "id-ID-GadisNeural": [],
2313
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
2314
+ "ja-JP-KeitaNeural": ["chat"],
2315
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
2316
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
2317
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
2318
+ "ms-MY-YasminNeural": [],
2319
+ "pt-BR-FranciscaNeural": ["calm"],
2320
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
2321
+ "th-TH-PremwadeeNeural": [],
2322
+ "vi-VN-HoaiMyNeural": [],
2323
+ "zh-CN-XiaoxiaoNeural": [
2324
+ "assistant",
2325
+ "chat",
2326
+ "customerservice",
2327
+ "newscast",
2328
+ "cheerful",
2329
+ "empathetic",
2330
+ "excited",
2331
+ "friendly",
2332
+ "hopeful",
2333
+ "sad",
2334
+ "terrified",
2335
+ "whispering",
2336
+ "poetry-reading",
2337
+ "sports_commentary",
2338
+ "sports_commentary_excited",
2339
+ "story"
2340
+ ],
2341
+ "zh-CN-YunxiNeural": [
2342
+ "narration-relaxed",
2343
+ "embarrassed",
2344
+ "fearful",
2345
+ "sad",
2346
+ "disgruntled",
2347
+ "serious",
2348
+ "angry",
2349
+ "depressed",
2350
+ "chat",
2351
+ "cheerful",
2352
+ "assistant"
2353
+ ],
2354
+ "zh-TW-HsiaoChenNeural": []
2355
+ };
2356
+
2357
+ // packages/ssml-editor-react/src/constants/ssmlPresets.ts
1551
2358
  var SSML_PRESETS = [
1552
2359
  {
1553
2360
  id: "basic",
@@ -1647,7 +2454,7 @@ function getExpressAsStyleCategory(style) {
1647
2454
  }
1648
2455
  return "other";
1649
2456
  }
1650
- var VOICE_STYLE_MAP = {
2457
+ var LEGACY_VOICE_STYLE_MAP = {
1651
2458
  "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
1652
2459
  "ja-JP-KeitaNeural": ["chat"],
1653
2460
  "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
@@ -1733,6 +2540,7 @@ var VOICE_STYLE_MAP = {
1733
2540
  "de-DE-ConradNeural": ["cheerful", "sad"],
1734
2541
  "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
1735
2542
  };
2543
+ var VOICE_STYLE_MAP = Object.keys(AZURE_VOICE_STYLE_MAP).length > 0 ? AZURE_VOICE_STYLE_MAP : LEGACY_VOICE_STYLE_MAP;
1736
2544
  var VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map(
1737
2545
  Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
1738
2546
  );
@@ -1775,6 +2583,7 @@ var SAY_AS_PRESETS = [
1775
2583
  ];
1776
2584
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
1777
2585
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
2586
+ var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
1778
2587
  var SILENCE_TYPE_PRESETS = [
1779
2588
  "Leading",
1780
2589
  "Tailing",
@@ -1783,6 +2592,11 @@ var SILENCE_TYPE_PRESETS = [
1783
2592
  "Semicolon",
1784
2593
  "Enumerationcomma"
1785
2594
  ];
2595
+ var AUDIO_DURATION_DESCRIPTIONS = {
2596
+ "5s": { ja: "5\u79D2", en: "5 seconds" },
2597
+ "10s": { ja: "10\u79D2", en: "10 seconds" },
2598
+ "30s": { ja: "30\u79D2", en: "30 seconds" }
2599
+ };
1786
2600
  var PHONEME_ALPHABET_PRESETS = ["ipa", "sapi", "ups", "x-sampa"];
1787
2601
  var VISEME_TYPE_PRESETS = ["redlips_front", "FacialExpression"];
1788
2602
  var SSML_ATTRIBUTE_PRESETS = {
@@ -1826,6 +2640,14 @@ var SSML_ATTRIBUTE_PRESETS = {
1826
2640
  type: SILENCE_TYPE_PRESETS,
1827
2641
  value: SILENCE_VALUE_PRESETS
1828
2642
  },
2643
+ "mstts:audioduration": {
2644
+ value: AUDIO_DURATION_PRESETS
2645
+ },
2646
+ "mstts:backgroundaudio": {
2647
+ volume: PROSODY_VOLUME_PRESETS,
2648
+ fadein: AUDIO_DURATION_PRESETS,
2649
+ fadeout: AUDIO_DURATION_PRESETS
2650
+ },
1829
2651
  silence: {
1830
2652
  type: SILENCE_TYPE_PRESETS,
1831
2653
  value: SILENCE_VALUE_PRESETS
@@ -2162,7 +2984,7 @@ var SILENCE_VALUE_DESCRIPTIONS = {
2162
2984
  };
2163
2985
 
2164
2986
  // packages/ssml-editor-react/src/ssmlContext.ts
2165
- function findTagEnd2(source, start, limit) {
2987
+ function findTagEnd3(source, start, limit) {
2166
2988
  let quote;
2167
2989
  for (let index = start + 1; index < limit; index += 1) {
2168
2990
  const character = source[index];
@@ -2212,7 +3034,7 @@ function findActiveSsmlTags(source, offset) {
2212
3034
  index = end;
2213
3035
  continue;
2214
3036
  }
2215
- const tagEnd = findTagEnd2(source, tagStart, source.length);
3037
+ const tagEnd = findTagEnd3(source, tagStart, source.length);
2216
3038
  const tag = source.slice(tagStart, tagEnd === -1 ? source.length : tagEnd + 1);
2217
3039
  const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
2218
3040
  const openingMatch = tag.match(/^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/);
@@ -2255,7 +3077,7 @@ function findSsmlVoiceContext(source, offset) {
2255
3077
  index = end === -1 || end + 2 > limit ? limit : end + 2;
2256
3078
  continue;
2257
3079
  }
2258
- const tagEnd = findTagEnd2(source, tagStart, limit);
3080
+ const tagEnd = findTagEnd3(source, tagStart, limit);
2259
3081
  if (tagEnd === -1) {
2260
3082
  break;
2261
3083
  }
@@ -2313,6 +3135,28 @@ var SSML_COMPLETION_SNIPPETS = [
2313
3135
  label: "mstts:express-as",
2314
3136
  insertText: `<mstts:express-as style="cheerful">\${1:text}</mstts:express-as>`
2315
3137
  },
3138
+ {
3139
+ label: "mstts:audioduration",
3140
+ insertText: '<mstts:audioduration value="10s" />'
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
+ },
2316
3160
  {
2317
3161
  label: "sub",
2318
3162
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -2580,6 +3424,13 @@ var SSML_HOVER_COPY = {
2580
3424
  parameters: {
2581
3425
  type: { title: "type", description: "\u30D3\u30BC\u30FC\u30E0\u30A4\u30D9\u30F3\u30C8\u306E\u5F62\u5F0F\u3002" }
2582
3426
  }
3427
+ },
3428
+ "mstts:audioduration": {
3429
+ title: "\u97F3\u58F0\u9577",
3430
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
3431
+ parameters: {
3432
+ value: { title: "value", description: "\u76EE\u6A19\u6642\u9593\u3002\u4F8B: `10s`\u3001`5000ms`\u3001`00:00:10`\u3002" }
3433
+ }
2583
3434
  }
2584
3435
  }
2585
3436
  },
@@ -2737,6 +3588,13 @@ var SSML_HOVER_COPY = {
2737
3588
  parameters: {
2738
3589
  type: { title: "type", description: "The viseme event format." }
2739
3590
  }
3591
+ },
3592
+ "mstts:audioduration": {
3593
+ title: "Audio duration",
3594
+ description: "Sets the target duration of synthesized audio.",
3595
+ parameters: {
3596
+ value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." }
3597
+ }
2740
3598
  }
2741
3599
  }
2742
3600
  }
@@ -3033,6 +3891,74 @@ var SSML_TAG_DEFINITIONS = [
3033
3891
  values: VISEME_TYPE_PRESETS2
3034
3892
  }
3035
3893
  ]
3894
+ },
3895
+ {
3896
+ name: "mstts:audioduration",
3897
+ description: "Sets the target duration of synthesized audio.",
3898
+ parameters: [
3899
+ {
3900
+ name: "value",
3901
+ description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`.",
3902
+ example: "10s"
3903
+ }
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: []
3036
3962
  }
3037
3963
  ];
3038
3964
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -3091,7 +4017,7 @@ function toRange(source, token) {
3091
4017
  function containsOffset(token, offset) {
3092
4018
  return offset >= token.start && offset < token.end;
3093
4019
  }
3094
- function findTagEnd3(source, start) {
4020
+ function findTagEnd4(source, start) {
3095
4021
  let quote;
3096
4022
  for (let index = start; index < source.length; index += 1) {
3097
4023
  const character = source[index];
@@ -3222,7 +4148,7 @@ function findTagAtOffset(source, offset) {
3222
4148
  searchStart = tokenEnd2;
3223
4149
  continue;
3224
4150
  }
3225
- const tagEnd = findTagEnd3(source, start + 1);
4151
+ const tagEnd = findTagEnd4(source, start + 1);
3226
4152
  const contentEnd = tagEnd ?? source.length;
3227
4153
  const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
3228
4154
  if (offset < tokenEnd) {
@@ -3574,13 +4500,34 @@ var SSML_INSERTIONS = [
3574
4500
  suffix: "",
3575
4501
  mode: "insert"
3576
4502
  })
4503
+ },
4504
+ {
4505
+ id: "mstts:audioduration",
4506
+ icon: "\u25F7",
4507
+ tagName: "mstts:audioduration",
4508
+ selfClosing: true,
4509
+ labels: { ja: "\u97F3\u58F0\u9577", en: "Audio duration" },
4510
+ descriptions: {
4511
+ ja: "\u5408\u6210\u97F3\u58F0\u306E\u76EE\u6A19\u6642\u9593\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
4512
+ en: "Sets the target duration of synthesized audio."
4513
+ },
4514
+ parameterDescription: {
4515
+ ja: "\u76EE\u6A19\u6642\u9593\u3092\u9078\u629E\u3057\u307E\u3059\u3002",
4516
+ en: "Selects the target duration."
4517
+ },
4518
+ options: createInsertionOptions(AUDIO_DURATION_PRESETS, AUDIO_DURATION_DESCRIPTIONS),
4519
+ createTemplate: (value) => ({
4520
+ prefix: `<mstts:audioduration value="${value}"/>`,
4521
+ suffix: "",
4522
+ mode: "insert"
4523
+ })
3577
4524
  }
3578
4525
  ];
3579
4526
  var DEFAULT_INSERTION_GROUPS = [
3580
4527
  {
3581
4528
  id: "pauses",
3582
4529
  labels: { ja: "\u9593\u30FB\u7121\u97F3", en: "Pauses" },
3583
- insertionIds: ["break", "mstts:silence"]
4530
+ insertionIds: ["break", "mstts:silence", "mstts:audioduration"]
3584
4531
  },
3585
4532
  {
3586
4533
  id: "prosody",
@@ -3817,6 +4764,60 @@ section[data-ssml-editor] {
3817
4764
  border-radius: 0.25rem;
3818
4765
  overflow: visible;
3819
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
+ }
3820
4821
  `.trim();
3821
4822
  function injectStyles() {
3822
4823
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
@@ -3830,6 +4831,55 @@ function injectStyles() {
3830
4831
  function isDarkTheme(theme) {
3831
4832
  return theme === "vs-dark" || theme.toLowerCase().includes("dark");
3832
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
+ }
3833
4883
  function getMenuPosition(trigger, menu) {
3834
4884
  const bounds = trigger.getBoundingClientRect();
3835
4885
  const margin = 8;
@@ -3851,6 +4901,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3851
4901
  this.toolbarActions = null;
3852
4902
  this.display = null;
3853
4903
  this.editorContainer = null;
4904
+ this.visualContainer = null;
3854
4905
  this.helpPanel = null;
3855
4906
  this.openMenu = null;
3856
4907
  this.openMenuTrigger = null;
@@ -3864,6 +4915,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
3864
4915
  this.documentState = null;
3865
4916
  this.decorationsVisible = false;
3866
4917
  this.helpOpen = false;
4918
+ this.visualSelectedPath = null;
4919
+ this.visualSelection = { start: 0, end: 0 };
3867
4920
  this.handleDocumentPointerDown = (event) => {
3868
4921
  const target = event.target;
3869
4922
  if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
@@ -3905,6 +4958,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
3905
4958
  set locale(locale) {
3906
4959
  this.setAttribute("locale", locale);
3907
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
+ }
3908
4967
  prepareDocument(value) {
3909
4968
  try {
3910
4969
  this.documentState = parseSsml(value);
@@ -3947,6 +5006,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3947
5006
  }
3948
5007
  }
3949
5008
  }
5009
+ this.renderVisualEditor();
3950
5010
  return;
3951
5011
  }
3952
5012
  if (name === "theme" && this.monaco) {
@@ -3964,6 +5024,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3964
5024
  this.renderHelp();
3965
5025
  return;
3966
5026
  }
5027
+ if (name === "edit-mode") {
5028
+ this.updateEditMode();
5029
+ }
3967
5030
  if (name === "show-decorations") {
3968
5031
  this.decorationsVisible = newValue !== null;
3969
5032
  this.updateDecorations();
@@ -3988,7 +5051,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3988
5051
  display.dataset.ssmlEditorDisplay = "";
3989
5052
  const editorContainer = document.createElement("div");
3990
5053
  editorContainer.className = "ssml-editor-editor";
3991
- display.append(editorContainer);
5054
+ const visualContainer = document.createElement("div");
5055
+ visualContainer.className = "ssml-editor-visual";
5056
+ display.append(editorContainer, visualContainer);
3992
5057
  root.append(toolbar, display);
3993
5058
  this.replaceChildren(root);
3994
5059
  this.root = root;
@@ -3996,8 +5061,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
3996
5061
  this.toolbarActions = toolbarActions;
3997
5062
  this.display = display;
3998
5063
  this.editorContainer = editorContainer;
5064
+ this.visualContainer = visualContainer;
3999
5065
  this.renderToolbar();
4000
5066
  this.renderHelp();
5067
+ this.updateEditMode();
5068
+ this.renderVisualEditor();
4001
5069
  }
4002
5070
  renderToolbar() {
4003
5071
  const toolbar = this.toolbar;
@@ -4041,10 +5109,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
4041
5109
  for (const id of toolbarIds) {
4042
5110
  const group = groupByButtonId.get(id);
4043
5111
  if (previousGroup !== void 0 && group !== previousGroup) {
4044
- const separator = document.createElement("span");
4045
- separator.className = "ssml-editor-toolbar-separator";
4046
- separator.setAttribute("aria-hidden", "true");
4047
- toolbarActions.append(separator);
5112
+ const separator2 = document.createElement("span");
5113
+ separator2.className = "ssml-editor-toolbar-separator";
5114
+ separator2.setAttribute("aria-hidden", "true");
5115
+ toolbarActions.append(separator2);
4048
5116
  }
4049
5117
  previousGroup = group;
4050
5118
  const insertion = insertionById.get(id);
@@ -4056,8 +5124,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
4056
5124
  toolbarActions.append(this.createActionButton(id));
4057
5125
  }
4058
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"));
4059
5131
  this.updateActiveButtons();
4060
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
+ }
4061
5145
  createActionButton(id) {
4062
5146
  const copy = EDITOR_COPY[this.locale];
4063
5147
  const labels = {
@@ -4437,6 +5521,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
4437
5521
  this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
4438
5522
  }
4439
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
+ }
4440
5668
  updateActiveButtons() {
4441
5669
  const editor = this.editor;
4442
5670
  const model = this.model;
@@ -4464,6 +5692,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
4464
5692
  return;
4465
5693
  }
4466
5694
  const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
5695
+ this.renderVisualEditor();
4467
5696
  const editor = monacoModule.editor.create(container, {
4468
5697
  model,
4469
5698
  theme: this.theme,
@@ -4562,7 +5791,8 @@ SsmlEditorElement.observedAttributes = [
4562
5791
  "locale",
4563
5792
  "show-toolbar",
4564
5793
  "show-toolbar-labels",
4565
- "show-decorations"
5794
+ "show-decorations",
5795
+ "edit-mode"
4566
5796
  ];
4567
5797
 
4568
5798
  // packages/ssml-editor-elements/src/index.ts