ssml-builder-js 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -8
- package/dist/{chunk-VCDMKVVT.mjs → chunk-7LCSH4SZ.mjs} +457 -25
- package/dist/chunk-7LCSH4SZ.mjs.map +1 -0
- package/dist/{chunk-MWSDFGXW.mjs → chunk-JNJVTEL6.mjs} +785 -9
- package/dist/chunk-JNJVTEL6.mjs.map +1 -0
- package/dist/core.d.mts +91 -3
- package/dist/core.d.ts +91 -3
- package/dist/core.js +462 -25
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +13 -3
- package/dist/elements.d.mts +10 -1
- package/dist/elements.d.ts +10 -1
- package/dist/elements.js +1071 -13
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +292 -8
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +462 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +40 -3
- package/dist/react.d.ts +40 -3
- package/dist/react.js +1026 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +237 -5
- package/dist/react.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-MWSDFGXW.mjs.map +0 -1
- package/dist/chunk-VCDMKVVT.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -44,13 +44,18 @@ __export(src_exports, {
|
|
|
44
44
|
buildPartialSsml: () => buildPartialSsml,
|
|
45
45
|
buildSsml: () => buildSsml,
|
|
46
46
|
extractSsmlText: () => extractSsmlText,
|
|
47
|
+
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
48
|
+
fromPlainTextToSsml: () => fromPlainTextToSsml,
|
|
49
|
+
getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
|
|
50
|
+
getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
|
|
47
51
|
isValidAzureAudioDuration: () => isValidAzureAudioDuration,
|
|
48
52
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
49
53
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
50
54
|
parseSsml: () => parseSsml,
|
|
51
55
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
52
56
|
validateAzureSsml: () => validateAzureSsml,
|
|
53
|
-
validateSsml: () => validateSsml
|
|
57
|
+
validateSsml: () => validateSsml,
|
|
58
|
+
validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
|
|
54
59
|
});
|
|
55
60
|
module.exports = __toCommonJS(src_exports);
|
|
56
61
|
|
|
@@ -87,7 +92,13 @@ var SSML_TAGS = {
|
|
|
87
92
|
SILENCE: "silence",
|
|
88
93
|
MSTTS_VISEME: "mstts:viseme",
|
|
89
94
|
VISEME: "viseme",
|
|
90
|
-
MSTTS_AUDIO_DURATION: "mstts:audioduration"
|
|
95
|
+
MSTTS_AUDIO_DURATION: "mstts:audioduration",
|
|
96
|
+
MSTTS_DIALOG: "mstts:dialog",
|
|
97
|
+
MSTTS_TURN: "mstts:turn",
|
|
98
|
+
MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
|
|
99
|
+
MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
|
|
100
|
+
MSTTS_EMBEDDING: "mstts:embedding",
|
|
101
|
+
MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
|
|
91
102
|
};
|
|
92
103
|
var SSML_ATTRS = {
|
|
93
104
|
VERSION: "version",
|
|
@@ -96,6 +107,7 @@ var SSML_ATTRS = {
|
|
|
96
107
|
LANG: "lang",
|
|
97
108
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
98
109
|
NAME: "name",
|
|
110
|
+
VOICE: "voice",
|
|
99
111
|
EFFECT: "effect",
|
|
100
112
|
RATE: "rate",
|
|
101
113
|
PITCH: "pitch",
|
|
@@ -127,7 +139,9 @@ var SSML_ATTRS = {
|
|
|
127
139
|
MARK: "mark",
|
|
128
140
|
URI: "uri",
|
|
129
141
|
TYPE: "type",
|
|
130
|
-
VALUE: "value"
|
|
142
|
+
VALUE: "value",
|
|
143
|
+
FADE_IN: "fadein",
|
|
144
|
+
FADE_OUT: "fadeout"
|
|
131
145
|
};
|
|
132
146
|
|
|
133
147
|
// packages/ssml-core/src/builder.ts
|
|
@@ -219,6 +233,20 @@ function getAttributes(element) {
|
|
|
219
233
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
220
234
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
221
235
|
break;
|
|
236
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
237
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
238
|
+
break;
|
|
239
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
240
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
241
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
242
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
243
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
244
|
+
break;
|
|
245
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
246
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
247
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
248
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
249
|
+
break;
|
|
222
250
|
case SSML_TAGS.PARAGRAPH:
|
|
223
251
|
case SSML_TAGS.SENTENCE:
|
|
224
252
|
case SSML_TAGS.WORD:
|
|
@@ -785,6 +813,40 @@ function convertElement(node) {
|
|
|
785
813
|
if (value !== void 0) element.value = value;
|
|
786
814
|
return finishElement(element, node, attributes);
|
|
787
815
|
}
|
|
816
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
817
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
818
|
+
return finishElement(element, node, attributes);
|
|
819
|
+
}
|
|
820
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
821
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
822
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
823
|
+
if (voice !== void 0) element.voice = voice;
|
|
824
|
+
return finishElement(element, node, attributes);
|
|
825
|
+
}
|
|
826
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
827
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
828
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
829
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
830
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
831
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
832
|
+
if (src !== void 0) element.src = src;
|
|
833
|
+
if (volume !== void 0) element.volume = volume;
|
|
834
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
835
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
836
|
+
return finishElement(element, node, attributes);
|
|
837
|
+
}
|
|
838
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
839
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
840
|
+
return finishElement(element, node, attributes);
|
|
841
|
+
}
|
|
842
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
843
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
844
|
+
return finishElement(element, node, attributes);
|
|
845
|
+
}
|
|
846
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
847
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
848
|
+
return finishElement(element, node, attributes);
|
|
849
|
+
}
|
|
788
850
|
default: {
|
|
789
851
|
const element = {
|
|
790
852
|
name: node.name,
|
|
@@ -1059,6 +1121,283 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1059
1121
|
return result + ssml.slice(cursor);
|
|
1060
1122
|
}
|
|
1061
1123
|
|
|
1124
|
+
// packages/ssml-core/src/migration.ts
|
|
1125
|
+
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1126
|
+
function elementName(element) {
|
|
1127
|
+
switch (element.type) {
|
|
1128
|
+
case "custom":
|
|
1129
|
+
case "element":
|
|
1130
|
+
return element.name;
|
|
1131
|
+
case "expressAs":
|
|
1132
|
+
return "mstts:express-as";
|
|
1133
|
+
case "sayAs":
|
|
1134
|
+
return "say-as";
|
|
1135
|
+
case "silence":
|
|
1136
|
+
return "mstts:silence";
|
|
1137
|
+
case "viseme":
|
|
1138
|
+
return "mstts:viseme";
|
|
1139
|
+
default:
|
|
1140
|
+
return element.type;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function addAttribute2(attributes, name, value) {
|
|
1144
|
+
if (value !== void 0) attributes[name] = value;
|
|
1145
|
+
}
|
|
1146
|
+
function elementAttributes(element) {
|
|
1147
|
+
const attributes = { ...element.attributes ?? {} };
|
|
1148
|
+
switch (element.type) {
|
|
1149
|
+
case "voice":
|
|
1150
|
+
addAttribute2(attributes, "name", element.name);
|
|
1151
|
+
addAttribute2(attributes, "effect", element.effect);
|
|
1152
|
+
break;
|
|
1153
|
+
case "prosody":
|
|
1154
|
+
addAttribute2(attributes, "rate", element.rate);
|
|
1155
|
+
addAttribute2(attributes, "pitch", element.pitch);
|
|
1156
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1157
|
+
addAttribute2(attributes, "contour", element.contour);
|
|
1158
|
+
addAttribute2(attributes, "range", element.range);
|
|
1159
|
+
break;
|
|
1160
|
+
case "break":
|
|
1161
|
+
addAttribute2(attributes, "time", element.time);
|
|
1162
|
+
addAttribute2(attributes, "strength", element.strength);
|
|
1163
|
+
break;
|
|
1164
|
+
case "express-as":
|
|
1165
|
+
case "expressAs":
|
|
1166
|
+
case "mstts:express-as":
|
|
1167
|
+
addAttribute2(attributes, "style", element.style);
|
|
1168
|
+
addAttribute2(attributes, "styledegree", element.styleDegree);
|
|
1169
|
+
addAttribute2(attributes, "role", element.role);
|
|
1170
|
+
break;
|
|
1171
|
+
case "say-as":
|
|
1172
|
+
case "sayAs":
|
|
1173
|
+
addAttribute2(attributes, "interpret-as", element.interpretAs);
|
|
1174
|
+
addAttribute2(attributes, "format", element.format);
|
|
1175
|
+
addAttribute2(attributes, "detail", element.detail);
|
|
1176
|
+
break;
|
|
1177
|
+
case "phoneme":
|
|
1178
|
+
addAttribute2(attributes, "alphabet", element.alphabet);
|
|
1179
|
+
addAttribute2(attributes, "ph", element.ph);
|
|
1180
|
+
break;
|
|
1181
|
+
case "emphasis":
|
|
1182
|
+
addAttribute2(attributes, "level", element.level);
|
|
1183
|
+
break;
|
|
1184
|
+
case "audio":
|
|
1185
|
+
addAttribute2(attributes, "src", element.src);
|
|
1186
|
+
addAttribute2(attributes, "desc", element.desc);
|
|
1187
|
+
addAttribute2(attributes, "clipBegin", element.clipBegin);
|
|
1188
|
+
addAttribute2(attributes, "clipEnd", element.clipEnd);
|
|
1189
|
+
addAttribute2(attributes, "speed", element.speed);
|
|
1190
|
+
addAttribute2(attributes, "repeatCount", element.repeatCount);
|
|
1191
|
+
addAttribute2(attributes, "repeatDuration", element.repeatDuration);
|
|
1192
|
+
addAttribute2(attributes, "soundLevel", element.soundLevel);
|
|
1193
|
+
break;
|
|
1194
|
+
case "sub":
|
|
1195
|
+
addAttribute2(attributes, "alias", element.alias);
|
|
1196
|
+
break;
|
|
1197
|
+
case "lang":
|
|
1198
|
+
addAttribute2(attributes, "xml:lang", element.lang);
|
|
1199
|
+
break;
|
|
1200
|
+
case "mark":
|
|
1201
|
+
addAttribute2(attributes, "name", element.name);
|
|
1202
|
+
break;
|
|
1203
|
+
case "bookmark":
|
|
1204
|
+
addAttribute2(attributes, "mark", element.mark);
|
|
1205
|
+
break;
|
|
1206
|
+
case "lexicon":
|
|
1207
|
+
addAttribute2(attributes, "uri", element.uri);
|
|
1208
|
+
break;
|
|
1209
|
+
case "mstts:silence":
|
|
1210
|
+
case "silence":
|
|
1211
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.silenceType);
|
|
1212
|
+
addAttribute2(attributes, "value", element.value);
|
|
1213
|
+
break;
|
|
1214
|
+
case "mstts:viseme":
|
|
1215
|
+
case "viseme":
|
|
1216
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.visemeType);
|
|
1217
|
+
break;
|
|
1218
|
+
case "mstts:audioduration":
|
|
1219
|
+
addAttribute2(attributes, "value", element.value);
|
|
1220
|
+
break;
|
|
1221
|
+
case "mstts:turn":
|
|
1222
|
+
addAttribute2(attributes, "voice", element.voice);
|
|
1223
|
+
break;
|
|
1224
|
+
case "mstts:backgroundaudio":
|
|
1225
|
+
addAttribute2(attributes, "src", element.src);
|
|
1226
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1227
|
+
addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
|
|
1228
|
+
addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
|
|
1232
|
+
}
|
|
1233
|
+
function childrenOf(node) {
|
|
1234
|
+
return node.children ?? [];
|
|
1235
|
+
}
|
|
1236
|
+
function extractSsmlTranslatableText(ssml, options = {}) {
|
|
1237
|
+
const document = parseSsml(ssml);
|
|
1238
|
+
const skipTags = new Set((options.skipTags ?? DEFAULT_TRANSLATION_SKIP_TAGS).map((tag) => tag.toLowerCase()));
|
|
1239
|
+
const result = [];
|
|
1240
|
+
const visit = (nodes, ancestors, path) => {
|
|
1241
|
+
nodes.forEach((node, index) => {
|
|
1242
|
+
if (typeof node === "string") {
|
|
1243
|
+
if (options.includeWhitespace || node.trim().length > 0) {
|
|
1244
|
+
const context = {
|
|
1245
|
+
ancestorTags: [...ancestors],
|
|
1246
|
+
parentAttributes: {},
|
|
1247
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1248
|
+
path: [...path, String(index)]
|
|
1249
|
+
};
|
|
1250
|
+
if (options.filter?.(context) ?? true) result.push(node);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (node.type === "text") {
|
|
1255
|
+
if (options.includeWhitespace || node.value.trim().length > 0) {
|
|
1256
|
+
const context = {
|
|
1257
|
+
ancestorTags: [...ancestors],
|
|
1258
|
+
parentAttributes: {},
|
|
1259
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1260
|
+
path: [...path, String(index)]
|
|
1261
|
+
};
|
|
1262
|
+
if (options.filter?.(context) ?? true) result.push(node.value);
|
|
1263
|
+
}
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const tag = elementName(node);
|
|
1267
|
+
if (skipTags.has(tag.toLowerCase())) return;
|
|
1268
|
+
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
visit(childrenOf(document), ["speak"], []);
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
function splitSentences(text) {
|
|
1275
|
+
const sentences = [];
|
|
1276
|
+
let start = 0;
|
|
1277
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1278
|
+
const character = text[index];
|
|
1279
|
+
const isTerminator = "\u3002\uFF01\uFF1F!?".includes(character) || character === "." && /\s|$/.test(text[index + 1] ?? "");
|
|
1280
|
+
if (isTerminator) {
|
|
1281
|
+
const value = text.slice(start, index + 1).trim();
|
|
1282
|
+
if (value) sentences.push(value);
|
|
1283
|
+
start = index + 1;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const tail = text.slice(start).trim();
|
|
1287
|
+
if (tail) sentences.push(tail);
|
|
1288
|
+
return sentences;
|
|
1289
|
+
}
|
|
1290
|
+
function fromPlainTextToSsml(text, options = {}) {
|
|
1291
|
+
if (typeof text !== "string") throw new TypeError("Plain text must be a string");
|
|
1292
|
+
const paragraphs = text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
|
|
1293
|
+
const useSentences = options.splitSentences ?? options.includeSentences ?? true;
|
|
1294
|
+
const paragraphNodes = paragraphs.map((paragraph) => ({
|
|
1295
|
+
type: "p",
|
|
1296
|
+
children: useSentences ? splitSentences(paragraph).map((sentence) => ({ type: "s", children: [sentence] })) : [paragraph]
|
|
1297
|
+
}));
|
|
1298
|
+
const voiceName = options.voice ?? options.voiceName;
|
|
1299
|
+
const children = voiceName ? [{ type: "voice", name: voiceName, children: paragraphNodes }] : paragraphNodes;
|
|
1300
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1301
|
+
${serializeDocument2({
|
|
1302
|
+
version: options.version ?? "1.0",
|
|
1303
|
+
lang: options.lang ?? options.language ?? "en-US",
|
|
1304
|
+
children
|
|
1305
|
+
})}`;
|
|
1306
|
+
}
|
|
1307
|
+
function serializeDocument2(document) {
|
|
1308
|
+
const attributes = [`version="${document.version}"`, `xml:lang="${document.lang}"`];
|
|
1309
|
+
const serialize = (node) => {
|
|
1310
|
+
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1311
|
+
if (node.type === "text") return serialize(node.value);
|
|
1312
|
+
const tag = elementName(node);
|
|
1313
|
+
const nodeAttributes = elementAttributes(node);
|
|
1314
|
+
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1315
|
+
const children = childrenOf(node).map(serialize).join("");
|
|
1316
|
+
return children ? `<${tag}${serializedAttributes}>${children}</${tag}>` : `<${tag}${serializedAttributes}/>`;
|
|
1317
|
+
};
|
|
1318
|
+
return `<speak ${attributes.join(" ")} xmlns="http://www.w3.org/2001/10/synthesis">${(document.children ?? []).map(serialize).join("")}</speak>`;
|
|
1319
|
+
}
|
|
1320
|
+
function flatten(document) {
|
|
1321
|
+
const result = [];
|
|
1322
|
+
const visit = (nodes, path) => {
|
|
1323
|
+
nodes.forEach((node, index) => {
|
|
1324
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1325
|
+
const currentPath = `${path}/${index}`;
|
|
1326
|
+
result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
|
|
1327
|
+
visit(childrenOf(node), currentPath);
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1330
|
+
result.push({ name: "speak", attributes: { version: document.version, "xml:lang": document.lang }, path: "0" });
|
|
1331
|
+
visit(childrenOf(document), "0");
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1334
|
+
function validateSsmlStructureIntegrity(originalSsml, translatedSsml) {
|
|
1335
|
+
const mismatches = [];
|
|
1336
|
+
let original;
|
|
1337
|
+
let translated;
|
|
1338
|
+
try {
|
|
1339
|
+
original = parseSsml(originalSsml);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
mismatches.push({ kind: "parse", message: `Original SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1342
|
+
return {
|
|
1343
|
+
isValid: false,
|
|
1344
|
+
valid: false,
|
|
1345
|
+
errors: mismatches.map((item) => item.message),
|
|
1346
|
+
mismatches,
|
|
1347
|
+
mismatchedTags: []
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
translated = parseSsml(translatedSsml);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
mismatches.push({ kind: "parse", message: `Translated SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1354
|
+
return {
|
|
1355
|
+
isValid: false,
|
|
1356
|
+
valid: false,
|
|
1357
|
+
errors: mismatches.map((item) => item.message),
|
|
1358
|
+
mismatches,
|
|
1359
|
+
mismatchedTags: []
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
const originalElements = flatten(original);
|
|
1363
|
+
const translatedElements = flatten(translated);
|
|
1364
|
+
const count = Math.max(originalElements.length, translatedElements.length);
|
|
1365
|
+
for (let index = 0; index < count; index += 1) {
|
|
1366
|
+
const originalElement = originalElements[index];
|
|
1367
|
+
const translatedElement = translatedElements[index];
|
|
1368
|
+
if (!originalElement || !translatedElement || originalElement.name !== translatedElement.name) {
|
|
1369
|
+
mismatches.push({
|
|
1370
|
+
kind: "element",
|
|
1371
|
+
message: `SSML element structure differs at index ${index}`,
|
|
1372
|
+
original: originalElement?.name,
|
|
1373
|
+
path: originalElement?.path ?? translatedElement?.path ?? String(index),
|
|
1374
|
+
translated: translatedElement?.name
|
|
1375
|
+
});
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const attributeNames = /* @__PURE__ */ new Set([
|
|
1379
|
+
...Object.keys(originalElement.attributes),
|
|
1380
|
+
...Object.keys(translatedElement.attributes)
|
|
1381
|
+
]);
|
|
1382
|
+
for (const attribute of attributeNames) {
|
|
1383
|
+
if (originalElement.attributes[attribute] !== translatedElement.attributes[attribute]) {
|
|
1384
|
+
mismatches.push({
|
|
1385
|
+
kind: "attribute",
|
|
1386
|
+
message: `Attribute ${attribute} differs on <${originalElement.name}>`,
|
|
1387
|
+
original: originalElement.attributes[attribute],
|
|
1388
|
+
path: originalElement.path,
|
|
1389
|
+
translated: translatedElement.attributes[attribute]
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const mismatchedTags = [
|
|
1395
|
+
...new Set(mismatches.flatMap((mismatch) => [mismatch.original, mismatch.translated].filter(Boolean)))
|
|
1396
|
+
];
|
|
1397
|
+
const errors = mismatches.map((mismatch) => mismatch.message);
|
|
1398
|
+
return { isValid: mismatches.length === 0, valid: mismatches.length === 0, errors, mismatches, mismatchedTags };
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1062
1401
|
// packages/ssml-core/src/generated/azureVoiceDefinitions.ts
|
|
1063
1402
|
var AZURE_VOICE_DEFINITIONS = [
|
|
1064
1403
|
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
@@ -1273,12 +1612,23 @@ function tokenizeElements(source) {
|
|
|
1273
1612
|
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1274
1613
|
}
|
|
1275
1614
|
const selfClosing = /\/\s*>$/.test(raw);
|
|
1615
|
+
const parent = openElements[openElements.length - 1];
|
|
1276
1616
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1277
|
-
|
|
1617
|
+
const tokenName = nameMatch[1];
|
|
1618
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1619
|
+
tokens.push({
|
|
1620
|
+
attributes,
|
|
1621
|
+
end,
|
|
1622
|
+
name: tokenName,
|
|
1623
|
+
parentName: parent?.name,
|
|
1624
|
+
parentVoiceName,
|
|
1625
|
+
selfClosing,
|
|
1626
|
+
start
|
|
1627
|
+
});
|
|
1278
1628
|
if (!selfClosing) {
|
|
1279
1629
|
openElements.push({
|
|
1280
|
-
name:
|
|
1281
|
-
voiceName:
|
|
1630
|
+
name: tokenName,
|
|
1631
|
+
voiceName: tokenVoiceName
|
|
1282
1632
|
});
|
|
1283
1633
|
}
|
|
1284
1634
|
index = end + 1;
|
|
@@ -1416,6 +1766,54 @@ function definitionMatchesLanguage(definition, voiceName, language, normalizeLan
|
|
|
1416
1766
|
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1417
1767
|
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1418
1768
|
}
|
|
1769
|
+
function canonicalTagName(name) {
|
|
1770
|
+
const normalized = name.toLowerCase();
|
|
1771
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1772
|
+
if (normalized === "sayas") return "say-as";
|
|
1773
|
+
return normalized;
|
|
1774
|
+
}
|
|
1775
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1776
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1777
|
+
return;
|
|
1778
|
+
const tagName = canonicalTagName(token.name);
|
|
1779
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1780
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1781
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1782
|
+
addDiagnostic(
|
|
1783
|
+
diagnostics,
|
|
1784
|
+
source,
|
|
1785
|
+
token.start,
|
|
1786
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1787
|
+
"error",
|
|
1788
|
+
"azure-unsupported-tag-for-voice"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1793
|
+
const src = attr(token, "src");
|
|
1794
|
+
if (!src) {
|
|
1795
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
let parsed;
|
|
1799
|
+
try {
|
|
1800
|
+
parsed = new URL(src);
|
|
1801
|
+
} catch {
|
|
1802
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1806
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1807
|
+
if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
|
|
1808
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1809
|
+
else if (!options.allowExternalAudio)
|
|
1810
|
+
addDiagnostic(
|
|
1811
|
+
diagnostics,
|
|
1812
|
+
source,
|
|
1813
|
+
token.start,
|
|
1814
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1419
1817
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1420
1818
|
const name = token.name.toLowerCase();
|
|
1421
1819
|
if (name === "voice" && !attr(token, "name")?.trim())
|
|
@@ -1535,29 +1933,33 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
1535
1933
|
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1536
1934
|
}
|
|
1537
1935
|
if (name === "audio") {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1936
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1937
|
+
}
|
|
1938
|
+
if (name === "mstts:turn") {
|
|
1939
|
+
if (!attr(token, "voice")?.trim())
|
|
1940
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
|
|
1941
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
1942
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
1943
|
+
}
|
|
1944
|
+
if (name === "mstts:backgroundaudio") {
|
|
1945
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
1946
|
+
const volume = attr(token, "volume");
|
|
1947
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
|
|
1948
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
1949
|
+
for (const [attribute, value] of [
|
|
1950
|
+
["fadein", attr(token, "fadein")],
|
|
1951
|
+
["fadeout", attr(token, "fadeout")]
|
|
1952
|
+
]) {
|
|
1953
|
+
if (value && !isValidAzureAudioDuration(value))
|
|
1553
1954
|
addDiagnostic(
|
|
1554
1955
|
diagnostics,
|
|
1555
1956
|
source,
|
|
1556
1957
|
token.start,
|
|
1557
|
-
`<
|
|
1558
|
-
"error"
|
|
1958
|
+
`<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
|
|
1559
1959
|
);
|
|
1560
1960
|
}
|
|
1961
|
+
if (!token.selfClosing)
|
|
1962
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1561
1963
|
}
|
|
1562
1964
|
}
|
|
1563
1965
|
function validateAzureSsml(ssml, options = {}) {
|
|
@@ -1623,12 +2025,42 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1623
2025
|
);
|
|
1624
2026
|
}
|
|
1625
2027
|
for (const token of tokens) {
|
|
1626
|
-
const
|
|
2028
|
+
const tokenName = token.name.toLowerCase();
|
|
2029
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1627
2030
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2031
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2032
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2033
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2034
|
+
addDiagnostic(
|
|
2035
|
+
diagnostics,
|
|
2036
|
+
ssml,
|
|
2037
|
+
token.start,
|
|
2038
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
2039
|
+
"error",
|
|
2040
|
+
"azure-unsupported-model-for-voice"
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
1628
2043
|
}
|
|
1629
2044
|
return diagnostics;
|
|
1630
2045
|
}
|
|
1631
2046
|
|
|
2047
|
+
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2048
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
2049
|
+
apiVersion: "2025-10-01",
|
|
2050
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
2051
|
+
regions: [],
|
|
2052
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
// packages/ssml-core/src/voiceCatalog.ts
|
|
2056
|
+
function getAzureVoiceCatalogMetadata() {
|
|
2057
|
+
return {
|
|
2058
|
+
...AZURE_VOICE_CATALOG_METADATA,
|
|
2059
|
+
regions: [...AZURE_VOICE_CATALOG_METADATA.regions]
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
2063
|
+
|
|
1632
2064
|
// packages/azure-tts-client/src/errors.ts
|
|
1633
2065
|
var AzureTtsError = class extends Error {
|
|
1634
2066
|
constructor(status, statusText, responseBody, requestId) {
|
|
@@ -1819,12 +2251,17 @@ _options = new WeakMap();
|
|
|
1819
2251
|
buildPartialSsml,
|
|
1820
2252
|
buildSsml,
|
|
1821
2253
|
extractSsmlText,
|
|
2254
|
+
extractSsmlTranslatableText,
|
|
2255
|
+
fromPlainTextToSsml,
|
|
2256
|
+
getAzureVoiceCatalogMetadata,
|
|
2257
|
+
getBuiltInVoiceCatalogMetadata,
|
|
1822
2258
|
isValidAzureAudioDuration,
|
|
1823
2259
|
mapSsmlTextNodes,
|
|
1824
2260
|
normalizeAzureLanguage,
|
|
1825
2261
|
parseSsml,
|
|
1826
2262
|
synthesizeSpeech,
|
|
1827
2263
|
validateAzureSsml,
|
|
1828
|
-
validateSsml
|
|
2264
|
+
validateSsml,
|
|
2265
|
+
validateSsmlStructureIntegrity
|
|
1829
2266
|
});
|
|
1830
2267
|
//# sourceMappingURL=index.js.map
|