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
|
@@ -37,7 +37,13 @@ var SSML_TAGS = {
|
|
|
37
37
|
SILENCE: "silence",
|
|
38
38
|
MSTTS_VISEME: "mstts:viseme",
|
|
39
39
|
VISEME: "viseme",
|
|
40
|
-
MSTTS_AUDIO_DURATION: "mstts:audioduration"
|
|
40
|
+
MSTTS_AUDIO_DURATION: "mstts:audioduration",
|
|
41
|
+
MSTTS_DIALOG: "mstts:dialog",
|
|
42
|
+
MSTTS_TURN: "mstts:turn",
|
|
43
|
+
MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
|
|
44
|
+
MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
|
|
45
|
+
MSTTS_EMBEDDING: "mstts:embedding",
|
|
46
|
+
MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
|
|
41
47
|
};
|
|
42
48
|
var SSML_ATTRS = {
|
|
43
49
|
VERSION: "version",
|
|
@@ -46,6 +52,7 @@ var SSML_ATTRS = {
|
|
|
46
52
|
LANG: "lang",
|
|
47
53
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
48
54
|
NAME: "name",
|
|
55
|
+
VOICE: "voice",
|
|
49
56
|
EFFECT: "effect",
|
|
50
57
|
RATE: "rate",
|
|
51
58
|
PITCH: "pitch",
|
|
@@ -77,7 +84,9 @@ var SSML_ATTRS = {
|
|
|
77
84
|
MARK: "mark",
|
|
78
85
|
URI: "uri",
|
|
79
86
|
TYPE: "type",
|
|
80
|
-
VALUE: "value"
|
|
87
|
+
VALUE: "value",
|
|
88
|
+
FADE_IN: "fadein",
|
|
89
|
+
FADE_OUT: "fadeout"
|
|
81
90
|
};
|
|
82
91
|
|
|
83
92
|
// packages/ssml-core/src/builder.ts
|
|
@@ -169,6 +178,20 @@ function getAttributes(element) {
|
|
|
169
178
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
170
179
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
171
180
|
break;
|
|
181
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
182
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
183
|
+
break;
|
|
184
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
185
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
186
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
187
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
188
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
189
|
+
break;
|
|
190
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
191
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
192
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
193
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
194
|
+
break;
|
|
172
195
|
case SSML_TAGS.PARAGRAPH:
|
|
173
196
|
case SSML_TAGS.SENTENCE:
|
|
174
197
|
case SSML_TAGS.WORD:
|
|
@@ -735,6 +758,40 @@ function convertElement(node) {
|
|
|
735
758
|
if (value !== void 0) element.value = value;
|
|
736
759
|
return finishElement(element, node, attributes);
|
|
737
760
|
}
|
|
761
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
762
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
763
|
+
return finishElement(element, node, attributes);
|
|
764
|
+
}
|
|
765
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
766
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
767
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
768
|
+
if (voice !== void 0) element.voice = voice;
|
|
769
|
+
return finishElement(element, node, attributes);
|
|
770
|
+
}
|
|
771
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
772
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
773
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
774
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
775
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
776
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
777
|
+
if (src !== void 0) element.src = src;
|
|
778
|
+
if (volume !== void 0) element.volume = volume;
|
|
779
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
780
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
781
|
+
return finishElement(element, node, attributes);
|
|
782
|
+
}
|
|
783
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
784
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
785
|
+
return finishElement(element, node, attributes);
|
|
786
|
+
}
|
|
787
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
788
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
789
|
+
return finishElement(element, node, attributes);
|
|
790
|
+
}
|
|
791
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
792
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
793
|
+
return finishElement(element, node, attributes);
|
|
794
|
+
}
|
|
738
795
|
default: {
|
|
739
796
|
const element = {
|
|
740
797
|
name: node.name,
|
|
@@ -1009,6 +1066,283 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1009
1066
|
return result + ssml.slice(cursor);
|
|
1010
1067
|
}
|
|
1011
1068
|
|
|
1069
|
+
// packages/ssml-core/src/migration.ts
|
|
1070
|
+
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1071
|
+
function elementName(element) {
|
|
1072
|
+
switch (element.type) {
|
|
1073
|
+
case "custom":
|
|
1074
|
+
case "element":
|
|
1075
|
+
return element.name;
|
|
1076
|
+
case "expressAs":
|
|
1077
|
+
return "mstts:express-as";
|
|
1078
|
+
case "sayAs":
|
|
1079
|
+
return "say-as";
|
|
1080
|
+
case "silence":
|
|
1081
|
+
return "mstts:silence";
|
|
1082
|
+
case "viseme":
|
|
1083
|
+
return "mstts:viseme";
|
|
1084
|
+
default:
|
|
1085
|
+
return element.type;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function addAttribute2(attributes, name, value) {
|
|
1089
|
+
if (value !== void 0) attributes[name] = value;
|
|
1090
|
+
}
|
|
1091
|
+
function elementAttributes(element) {
|
|
1092
|
+
const attributes = { ...element.attributes ?? {} };
|
|
1093
|
+
switch (element.type) {
|
|
1094
|
+
case "voice":
|
|
1095
|
+
addAttribute2(attributes, "name", element.name);
|
|
1096
|
+
addAttribute2(attributes, "effect", element.effect);
|
|
1097
|
+
break;
|
|
1098
|
+
case "prosody":
|
|
1099
|
+
addAttribute2(attributes, "rate", element.rate);
|
|
1100
|
+
addAttribute2(attributes, "pitch", element.pitch);
|
|
1101
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1102
|
+
addAttribute2(attributes, "contour", element.contour);
|
|
1103
|
+
addAttribute2(attributes, "range", element.range);
|
|
1104
|
+
break;
|
|
1105
|
+
case "break":
|
|
1106
|
+
addAttribute2(attributes, "time", element.time);
|
|
1107
|
+
addAttribute2(attributes, "strength", element.strength);
|
|
1108
|
+
break;
|
|
1109
|
+
case "express-as":
|
|
1110
|
+
case "expressAs":
|
|
1111
|
+
case "mstts:express-as":
|
|
1112
|
+
addAttribute2(attributes, "style", element.style);
|
|
1113
|
+
addAttribute2(attributes, "styledegree", element.styleDegree);
|
|
1114
|
+
addAttribute2(attributes, "role", element.role);
|
|
1115
|
+
break;
|
|
1116
|
+
case "say-as":
|
|
1117
|
+
case "sayAs":
|
|
1118
|
+
addAttribute2(attributes, "interpret-as", element.interpretAs);
|
|
1119
|
+
addAttribute2(attributes, "format", element.format);
|
|
1120
|
+
addAttribute2(attributes, "detail", element.detail);
|
|
1121
|
+
break;
|
|
1122
|
+
case "phoneme":
|
|
1123
|
+
addAttribute2(attributes, "alphabet", element.alphabet);
|
|
1124
|
+
addAttribute2(attributes, "ph", element.ph);
|
|
1125
|
+
break;
|
|
1126
|
+
case "emphasis":
|
|
1127
|
+
addAttribute2(attributes, "level", element.level);
|
|
1128
|
+
break;
|
|
1129
|
+
case "audio":
|
|
1130
|
+
addAttribute2(attributes, "src", element.src);
|
|
1131
|
+
addAttribute2(attributes, "desc", element.desc);
|
|
1132
|
+
addAttribute2(attributes, "clipBegin", element.clipBegin);
|
|
1133
|
+
addAttribute2(attributes, "clipEnd", element.clipEnd);
|
|
1134
|
+
addAttribute2(attributes, "speed", element.speed);
|
|
1135
|
+
addAttribute2(attributes, "repeatCount", element.repeatCount);
|
|
1136
|
+
addAttribute2(attributes, "repeatDuration", element.repeatDuration);
|
|
1137
|
+
addAttribute2(attributes, "soundLevel", element.soundLevel);
|
|
1138
|
+
break;
|
|
1139
|
+
case "sub":
|
|
1140
|
+
addAttribute2(attributes, "alias", element.alias);
|
|
1141
|
+
break;
|
|
1142
|
+
case "lang":
|
|
1143
|
+
addAttribute2(attributes, "xml:lang", element.lang);
|
|
1144
|
+
break;
|
|
1145
|
+
case "mark":
|
|
1146
|
+
addAttribute2(attributes, "name", element.name);
|
|
1147
|
+
break;
|
|
1148
|
+
case "bookmark":
|
|
1149
|
+
addAttribute2(attributes, "mark", element.mark);
|
|
1150
|
+
break;
|
|
1151
|
+
case "lexicon":
|
|
1152
|
+
addAttribute2(attributes, "uri", element.uri);
|
|
1153
|
+
break;
|
|
1154
|
+
case "mstts:silence":
|
|
1155
|
+
case "silence":
|
|
1156
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.silenceType);
|
|
1157
|
+
addAttribute2(attributes, "value", element.value);
|
|
1158
|
+
break;
|
|
1159
|
+
case "mstts:viseme":
|
|
1160
|
+
case "viseme":
|
|
1161
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.visemeType);
|
|
1162
|
+
break;
|
|
1163
|
+
case "mstts:audioduration":
|
|
1164
|
+
addAttribute2(attributes, "value", element.value);
|
|
1165
|
+
break;
|
|
1166
|
+
case "mstts:turn":
|
|
1167
|
+
addAttribute2(attributes, "voice", element.voice);
|
|
1168
|
+
break;
|
|
1169
|
+
case "mstts:backgroundaudio":
|
|
1170
|
+
addAttribute2(attributes, "src", element.src);
|
|
1171
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1172
|
+
addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
|
|
1173
|
+
addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
|
|
1177
|
+
}
|
|
1178
|
+
function childrenOf(node) {
|
|
1179
|
+
return node.children ?? [];
|
|
1180
|
+
}
|
|
1181
|
+
function extractSsmlTranslatableText(ssml, options = {}) {
|
|
1182
|
+
const document = parseSsml(ssml);
|
|
1183
|
+
const skipTags = new Set((options.skipTags ?? DEFAULT_TRANSLATION_SKIP_TAGS).map((tag) => tag.toLowerCase()));
|
|
1184
|
+
const result = [];
|
|
1185
|
+
const visit = (nodes, ancestors, path) => {
|
|
1186
|
+
nodes.forEach((node, index) => {
|
|
1187
|
+
if (typeof node === "string") {
|
|
1188
|
+
if (options.includeWhitespace || node.trim().length > 0) {
|
|
1189
|
+
const context = {
|
|
1190
|
+
ancestorTags: [...ancestors],
|
|
1191
|
+
parentAttributes: {},
|
|
1192
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1193
|
+
path: [...path, String(index)]
|
|
1194
|
+
};
|
|
1195
|
+
if (options.filter?.(context) ?? true) result.push(node);
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (node.type === "text") {
|
|
1200
|
+
if (options.includeWhitespace || node.value.trim().length > 0) {
|
|
1201
|
+
const context = {
|
|
1202
|
+
ancestorTags: [...ancestors],
|
|
1203
|
+
parentAttributes: {},
|
|
1204
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1205
|
+
path: [...path, String(index)]
|
|
1206
|
+
};
|
|
1207
|
+
if (options.filter?.(context) ?? true) result.push(node.value);
|
|
1208
|
+
}
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const tag = elementName(node);
|
|
1212
|
+
if (skipTags.has(tag.toLowerCase())) return;
|
|
1213
|
+
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1214
|
+
});
|
|
1215
|
+
};
|
|
1216
|
+
visit(childrenOf(document), ["speak"], []);
|
|
1217
|
+
return result;
|
|
1218
|
+
}
|
|
1219
|
+
function splitSentences(text) {
|
|
1220
|
+
const sentences = [];
|
|
1221
|
+
let start = 0;
|
|
1222
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1223
|
+
const character = text[index];
|
|
1224
|
+
const isTerminator = "\u3002\uFF01\uFF1F!?".includes(character) || character === "." && /\s|$/.test(text[index + 1] ?? "");
|
|
1225
|
+
if (isTerminator) {
|
|
1226
|
+
const value = text.slice(start, index + 1).trim();
|
|
1227
|
+
if (value) sentences.push(value);
|
|
1228
|
+
start = index + 1;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const tail = text.slice(start).trim();
|
|
1232
|
+
if (tail) sentences.push(tail);
|
|
1233
|
+
return sentences;
|
|
1234
|
+
}
|
|
1235
|
+
function fromPlainTextToSsml(text, options = {}) {
|
|
1236
|
+
if (typeof text !== "string") throw new TypeError("Plain text must be a string");
|
|
1237
|
+
const paragraphs = text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
|
|
1238
|
+
const useSentences = options.splitSentences ?? options.includeSentences ?? true;
|
|
1239
|
+
const paragraphNodes = paragraphs.map((paragraph) => ({
|
|
1240
|
+
type: "p",
|
|
1241
|
+
children: useSentences ? splitSentences(paragraph).map((sentence) => ({ type: "s", children: [sentence] })) : [paragraph]
|
|
1242
|
+
}));
|
|
1243
|
+
const voiceName = options.voice ?? options.voiceName;
|
|
1244
|
+
const children = voiceName ? [{ type: "voice", name: voiceName, children: paragraphNodes }] : paragraphNodes;
|
|
1245
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1246
|
+
${serializeDocument2({
|
|
1247
|
+
version: options.version ?? "1.0",
|
|
1248
|
+
lang: options.lang ?? options.language ?? "en-US",
|
|
1249
|
+
children
|
|
1250
|
+
})}`;
|
|
1251
|
+
}
|
|
1252
|
+
function serializeDocument2(document) {
|
|
1253
|
+
const attributes = [`version="${document.version}"`, `xml:lang="${document.lang}"`];
|
|
1254
|
+
const serialize = (node) => {
|
|
1255
|
+
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1256
|
+
if (node.type === "text") return serialize(node.value);
|
|
1257
|
+
const tag = elementName(node);
|
|
1258
|
+
const nodeAttributes = elementAttributes(node);
|
|
1259
|
+
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1260
|
+
const children = childrenOf(node).map(serialize).join("");
|
|
1261
|
+
return children ? `<${tag}${serializedAttributes}>${children}</${tag}>` : `<${tag}${serializedAttributes}/>`;
|
|
1262
|
+
};
|
|
1263
|
+
return `<speak ${attributes.join(" ")} xmlns="http://www.w3.org/2001/10/synthesis">${(document.children ?? []).map(serialize).join("")}</speak>`;
|
|
1264
|
+
}
|
|
1265
|
+
function flatten(document) {
|
|
1266
|
+
const result = [];
|
|
1267
|
+
const visit = (nodes, path) => {
|
|
1268
|
+
nodes.forEach((node, index) => {
|
|
1269
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1270
|
+
const currentPath = `${path}/${index}`;
|
|
1271
|
+
result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
|
|
1272
|
+
visit(childrenOf(node), currentPath);
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
result.push({ name: "speak", attributes: { version: document.version, "xml:lang": document.lang }, path: "0" });
|
|
1276
|
+
visit(childrenOf(document), "0");
|
|
1277
|
+
return result;
|
|
1278
|
+
}
|
|
1279
|
+
function validateSsmlStructureIntegrity(originalSsml, translatedSsml) {
|
|
1280
|
+
const mismatches = [];
|
|
1281
|
+
let original;
|
|
1282
|
+
let translated;
|
|
1283
|
+
try {
|
|
1284
|
+
original = parseSsml(originalSsml);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
mismatches.push({ kind: "parse", message: `Original SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1287
|
+
return {
|
|
1288
|
+
isValid: false,
|
|
1289
|
+
valid: false,
|
|
1290
|
+
errors: mismatches.map((item) => item.message),
|
|
1291
|
+
mismatches,
|
|
1292
|
+
mismatchedTags: []
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
translated = parseSsml(translatedSsml);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
mismatches.push({ kind: "parse", message: `Translated SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1299
|
+
return {
|
|
1300
|
+
isValid: false,
|
|
1301
|
+
valid: false,
|
|
1302
|
+
errors: mismatches.map((item) => item.message),
|
|
1303
|
+
mismatches,
|
|
1304
|
+
mismatchedTags: []
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
const originalElements = flatten(original);
|
|
1308
|
+
const translatedElements = flatten(translated);
|
|
1309
|
+
const count = Math.max(originalElements.length, translatedElements.length);
|
|
1310
|
+
for (let index = 0; index < count; index += 1) {
|
|
1311
|
+
const originalElement = originalElements[index];
|
|
1312
|
+
const translatedElement = translatedElements[index];
|
|
1313
|
+
if (!originalElement || !translatedElement || originalElement.name !== translatedElement.name) {
|
|
1314
|
+
mismatches.push({
|
|
1315
|
+
kind: "element",
|
|
1316
|
+
message: `SSML element structure differs at index ${index}`,
|
|
1317
|
+
original: originalElement?.name,
|
|
1318
|
+
path: originalElement?.path ?? translatedElement?.path ?? String(index),
|
|
1319
|
+
translated: translatedElement?.name
|
|
1320
|
+
});
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const attributeNames = /* @__PURE__ */ new Set([
|
|
1324
|
+
...Object.keys(originalElement.attributes),
|
|
1325
|
+
...Object.keys(translatedElement.attributes)
|
|
1326
|
+
]);
|
|
1327
|
+
for (const attribute of attributeNames) {
|
|
1328
|
+
if (originalElement.attributes[attribute] !== translatedElement.attributes[attribute]) {
|
|
1329
|
+
mismatches.push({
|
|
1330
|
+
kind: "attribute",
|
|
1331
|
+
message: `Attribute ${attribute} differs on <${originalElement.name}>`,
|
|
1332
|
+
original: originalElement.attributes[attribute],
|
|
1333
|
+
path: originalElement.path,
|
|
1334
|
+
translated: translatedElement.attributes[attribute]
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const mismatchedTags = [
|
|
1340
|
+
...new Set(mismatches.flatMap((mismatch) => [mismatch.original, mismatch.translated].filter(Boolean)))
|
|
1341
|
+
];
|
|
1342
|
+
const errors = mismatches.map((mismatch) => mismatch.message);
|
|
1343
|
+
return { isValid: mismatches.length === 0, valid: mismatches.length === 0, errors, mismatches, mismatchedTags };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1012
1346
|
// packages/ssml-core/src/generated/azureVoiceDefinitions.ts
|
|
1013
1347
|
var AZURE_VOICE_DEFINITIONS = [
|
|
1014
1348
|
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
@@ -1223,12 +1557,23 @@ function tokenizeElements(source) {
|
|
|
1223
1557
|
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1224
1558
|
}
|
|
1225
1559
|
const selfClosing = /\/\s*>$/.test(raw);
|
|
1560
|
+
const parent = openElements[openElements.length - 1];
|
|
1226
1561
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1227
|
-
|
|
1562
|
+
const tokenName = nameMatch[1];
|
|
1563
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1564
|
+
tokens.push({
|
|
1565
|
+
attributes,
|
|
1566
|
+
end,
|
|
1567
|
+
name: tokenName,
|
|
1568
|
+
parentName: parent?.name,
|
|
1569
|
+
parentVoiceName,
|
|
1570
|
+
selfClosing,
|
|
1571
|
+
start
|
|
1572
|
+
});
|
|
1228
1573
|
if (!selfClosing) {
|
|
1229
1574
|
openElements.push({
|
|
1230
|
-
name:
|
|
1231
|
-
voiceName:
|
|
1575
|
+
name: tokenName,
|
|
1576
|
+
voiceName: tokenVoiceName
|
|
1232
1577
|
});
|
|
1233
1578
|
}
|
|
1234
1579
|
index = end + 1;
|
|
@@ -1366,6 +1711,54 @@ function definitionMatchesLanguage(definition, voiceName, language, normalizeLan
|
|
|
1366
1711
|
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1367
1712
|
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1368
1713
|
}
|
|
1714
|
+
function canonicalTagName(name) {
|
|
1715
|
+
const normalized = name.toLowerCase();
|
|
1716
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1717
|
+
if (normalized === "sayas") return "say-as";
|
|
1718
|
+
return normalized;
|
|
1719
|
+
}
|
|
1720
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1721
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1722
|
+
return;
|
|
1723
|
+
const tagName = canonicalTagName(token.name);
|
|
1724
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1725
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1726
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1727
|
+
addDiagnostic(
|
|
1728
|
+
diagnostics,
|
|
1729
|
+
source,
|
|
1730
|
+
token.start,
|
|
1731
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1732
|
+
"error",
|
|
1733
|
+
"azure-unsupported-tag-for-voice"
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1738
|
+
const src = attr(token, "src");
|
|
1739
|
+
if (!src) {
|
|
1740
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
let parsed;
|
|
1744
|
+
try {
|
|
1745
|
+
parsed = new URL(src);
|
|
1746
|
+
} catch {
|
|
1747
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1751
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1752
|
+
if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
|
|
1753
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1754
|
+
else if (!options.allowExternalAudio)
|
|
1755
|
+
addDiagnostic(
|
|
1756
|
+
diagnostics,
|
|
1757
|
+
source,
|
|
1758
|
+
token.start,
|
|
1759
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1369
1762
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1370
1763
|
const name = token.name.toLowerCase();
|
|
1371
1764
|
if (name === "voice" && !attr(token, "name")?.trim())
|
|
@@ -1485,29 +1878,33 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
1485
1878
|
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1486
1879
|
}
|
|
1487
1880
|
if (name === "audio") {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1881
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1882
|
+
}
|
|
1883
|
+
if (name === "mstts:turn") {
|
|
1884
|
+
if (!attr(token, "voice")?.trim())
|
|
1885
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
|
|
1886
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
1887
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
1888
|
+
}
|
|
1889
|
+
if (name === "mstts:backgroundaudio") {
|
|
1890
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
1891
|
+
const volume = attr(token, "volume");
|
|
1892
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
|
|
1893
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
1894
|
+
for (const [attribute, value] of [
|
|
1895
|
+
["fadein", attr(token, "fadein")],
|
|
1896
|
+
["fadeout", attr(token, "fadeout")]
|
|
1897
|
+
]) {
|
|
1898
|
+
if (value && !isValidAzureAudioDuration(value))
|
|
1503
1899
|
addDiagnostic(
|
|
1504
1900
|
diagnostics,
|
|
1505
1901
|
source,
|
|
1506
1902
|
token.start,
|
|
1507
|
-
`<
|
|
1508
|
-
"error"
|
|
1903
|
+
`<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
|
|
1509
1904
|
);
|
|
1510
1905
|
}
|
|
1906
|
+
if (!token.selfClosing)
|
|
1907
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1511
1908
|
}
|
|
1512
1909
|
}
|
|
1513
1910
|
function validateAzureSsml(ssml, options = {}) {
|
|
@@ -1573,12 +1970,42 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1573
1970
|
);
|
|
1574
1971
|
}
|
|
1575
1972
|
for (const token of tokens) {
|
|
1576
|
-
const
|
|
1973
|
+
const tokenName = token.name.toLowerCase();
|
|
1974
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1577
1975
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
1976
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
1977
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
1978
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
1979
|
+
addDiagnostic(
|
|
1980
|
+
diagnostics,
|
|
1981
|
+
ssml,
|
|
1982
|
+
token.start,
|
|
1983
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
1984
|
+
"error",
|
|
1985
|
+
"azure-unsupported-model-for-voice"
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1578
1988
|
}
|
|
1579
1989
|
return diagnostics;
|
|
1580
1990
|
}
|
|
1581
1991
|
|
|
1992
|
+
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
1993
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
1994
|
+
apiVersion: "2025-10-01",
|
|
1995
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
1996
|
+
regions: [],
|
|
1997
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// packages/ssml-core/src/voiceCatalog.ts
|
|
2001
|
+
function getAzureVoiceCatalogMetadata() {
|
|
2002
|
+
return {
|
|
2003
|
+
...AZURE_VOICE_CATALOG_METADATA,
|
|
2004
|
+
regions: [...AZURE_VOICE_CATALOG_METADATA.regions]
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
2008
|
+
|
|
1582
2009
|
export {
|
|
1583
2010
|
buildSsml,
|
|
1584
2011
|
parseSsml,
|
|
@@ -1586,9 +2013,14 @@ export {
|
|
|
1586
2013
|
validateSsml,
|
|
1587
2014
|
extractSsmlText,
|
|
1588
2015
|
mapSsmlTextNodes,
|
|
2016
|
+
extractSsmlTranslatableText,
|
|
2017
|
+
fromPlainTextToSsml,
|
|
2018
|
+
validateSsmlStructureIntegrity,
|
|
1589
2019
|
isValidAzureAudioDuration,
|
|
1590
2020
|
normalizeAzureLanguage,
|
|
1591
2021
|
areAzureLanguagesEquivalent,
|
|
1592
|
-
validateAzureSsml
|
|
2022
|
+
validateAzureSsml,
|
|
2023
|
+
getAzureVoiceCatalogMetadata,
|
|
2024
|
+
getBuiltInVoiceCatalogMetadata
|
|
1593
2025
|
};
|
|
1594
|
-
//# sourceMappingURL=chunk-
|
|
2026
|
+
//# sourceMappingURL=chunk-7LCSH4SZ.mjs.map
|