ssml-builder-js 2.8.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -12
- package/dist/{chunk-MWSDFGXW.mjs → chunk-BYIZQL2W.mjs} +854 -9
- package/dist/chunk-BYIZQL2W.mjs.map +1 -0
- package/dist/{chunk-VCDMKVVT.mjs → chunk-GW6CKHXF.mjs} +539 -25
- package/dist/chunk-GW6CKHXF.mjs.map +1 -0
- package/dist/core.d.mts +98 -3
- package/dist/core.d.ts +98 -3
- package/dist/core.js +544 -25
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +13 -3
- package/dist/elements.d.mts +10 -1
- package/dist/elements.d.ts +10 -1
- package/dist/elements.js +1140 -13
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +292 -8
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +544 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +47 -3
- package/dist/react.d.ts +47 -3
- package/dist/react.js +1095 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +237 -5
- package/dist/react.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-MWSDFGXW.mjs.map +0 -1
- package/dist/chunk-VCDMKVVT.mjs.map +0 -1
package/dist/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,8 @@ var SSML_ATTRS = {
|
|
|
96
107
|
LANG: "lang",
|
|
97
108
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
98
109
|
NAME: "name",
|
|
110
|
+
VOICE: "voice",
|
|
111
|
+
SPEAKER: "speaker",
|
|
99
112
|
EFFECT: "effect",
|
|
100
113
|
RATE: "rate",
|
|
101
114
|
PITCH: "pitch",
|
|
@@ -126,8 +139,15 @@ var SSML_ATTRS = {
|
|
|
126
139
|
ALIAS: "alias",
|
|
127
140
|
MARK: "mark",
|
|
128
141
|
URI: "uri",
|
|
142
|
+
ID: "id",
|
|
143
|
+
MODEL: "model",
|
|
144
|
+
PROFILE: "profile",
|
|
145
|
+
URL: "url",
|
|
146
|
+
SPEAKER_PROFILE_ID: "speakerProfileId",
|
|
129
147
|
TYPE: "type",
|
|
130
|
-
VALUE: "value"
|
|
148
|
+
VALUE: "value",
|
|
149
|
+
FADE_IN: "fadein",
|
|
150
|
+
FADE_OUT: "fadeout"
|
|
131
151
|
};
|
|
132
152
|
|
|
133
153
|
// packages/ssml-core/src/builder.ts
|
|
@@ -219,6 +239,30 @@ function getAttributes(element) {
|
|
|
219
239
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
220
240
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
221
241
|
break;
|
|
242
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
243
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
244
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
|
|
245
|
+
break;
|
|
246
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
247
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
248
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
249
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
250
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
251
|
+
break;
|
|
252
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
253
|
+
break;
|
|
254
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
255
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
256
|
+
break;
|
|
257
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
258
|
+
addAttribute(attributes, SSML_ATTRS.ID, element.id);
|
|
259
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
260
|
+
break;
|
|
261
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
262
|
+
addAttribute(attributes, SSML_ATTRS.URL, element.url);
|
|
263
|
+
addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
|
|
264
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
265
|
+
break;
|
|
222
266
|
case SSML_TAGS.PARAGRAPH:
|
|
223
267
|
case SSML_TAGS.SENTENCE:
|
|
224
268
|
case SSML_TAGS.WORD:
|
|
@@ -785,6 +829,54 @@ function convertElement(node) {
|
|
|
785
829
|
if (value !== void 0) element.value = value;
|
|
786
830
|
return finishElement(element, node, attributes);
|
|
787
831
|
}
|
|
832
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
833
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
834
|
+
return finishElement(element, node, attributes);
|
|
835
|
+
}
|
|
836
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
837
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
838
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
839
|
+
const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
|
|
840
|
+
if (voice !== void 0) element.voice = voice;
|
|
841
|
+
if (speaker !== void 0) element.speaker = speaker;
|
|
842
|
+
return finishElement(element, node, attributes);
|
|
843
|
+
}
|
|
844
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
845
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
846
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
847
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
848
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
849
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
850
|
+
if (src !== void 0) element.src = src;
|
|
851
|
+
if (volume !== void 0) element.volume = volume;
|
|
852
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
853
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
854
|
+
return finishElement(element, node, attributes);
|
|
855
|
+
}
|
|
856
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
857
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
858
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
859
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
860
|
+
return finishElement(element, node, attributes);
|
|
861
|
+
}
|
|
862
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
863
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
864
|
+
const id = readAttribute(attributes, SSML_ATTRS.ID);
|
|
865
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
866
|
+
if (id !== void 0) element.id = id;
|
|
867
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
868
|
+
return finishElement(element, node, attributes);
|
|
869
|
+
}
|
|
870
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
871
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
872
|
+
const url = readAttribute(attributes, SSML_ATTRS.URL);
|
|
873
|
+
const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
|
|
874
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
875
|
+
if (url !== void 0) element.url = url;
|
|
876
|
+
if (profile !== void 0) element.profile = profile;
|
|
877
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
878
|
+
return finishElement(element, node, attributes);
|
|
879
|
+
}
|
|
788
880
|
default: {
|
|
789
881
|
const element = {
|
|
790
882
|
name: node.name,
|
|
@@ -1059,6 +1151,296 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1059
1151
|
return result + ssml.slice(cursor);
|
|
1060
1152
|
}
|
|
1061
1153
|
|
|
1154
|
+
// packages/ssml-core/src/migration.ts
|
|
1155
|
+
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1156
|
+
function elementName(element) {
|
|
1157
|
+
switch (element.type) {
|
|
1158
|
+
case "custom":
|
|
1159
|
+
case "element":
|
|
1160
|
+
return element.name;
|
|
1161
|
+
case "expressAs":
|
|
1162
|
+
return "mstts:express-as";
|
|
1163
|
+
case "sayAs":
|
|
1164
|
+
return "say-as";
|
|
1165
|
+
case "silence":
|
|
1166
|
+
return "mstts:silence";
|
|
1167
|
+
case "viseme":
|
|
1168
|
+
return "mstts:viseme";
|
|
1169
|
+
default:
|
|
1170
|
+
return element.type;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function addAttribute2(attributes, name, value) {
|
|
1174
|
+
if (value !== void 0) attributes[name] = value;
|
|
1175
|
+
}
|
|
1176
|
+
function elementAttributes(element) {
|
|
1177
|
+
const attributes = { ...element.attributes ?? {} };
|
|
1178
|
+
switch (element.type) {
|
|
1179
|
+
case "voice":
|
|
1180
|
+
addAttribute2(attributes, "name", element.name);
|
|
1181
|
+
addAttribute2(attributes, "effect", element.effect);
|
|
1182
|
+
break;
|
|
1183
|
+
case "prosody":
|
|
1184
|
+
addAttribute2(attributes, "rate", element.rate);
|
|
1185
|
+
addAttribute2(attributes, "pitch", element.pitch);
|
|
1186
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1187
|
+
addAttribute2(attributes, "contour", element.contour);
|
|
1188
|
+
addAttribute2(attributes, "range", element.range);
|
|
1189
|
+
break;
|
|
1190
|
+
case "break":
|
|
1191
|
+
addAttribute2(attributes, "time", element.time);
|
|
1192
|
+
addAttribute2(attributes, "strength", element.strength);
|
|
1193
|
+
break;
|
|
1194
|
+
case "express-as":
|
|
1195
|
+
case "expressAs":
|
|
1196
|
+
case "mstts:express-as":
|
|
1197
|
+
addAttribute2(attributes, "style", element.style);
|
|
1198
|
+
addAttribute2(attributes, "styledegree", element.styleDegree);
|
|
1199
|
+
addAttribute2(attributes, "role", element.role);
|
|
1200
|
+
break;
|
|
1201
|
+
case "say-as":
|
|
1202
|
+
case "sayAs":
|
|
1203
|
+
addAttribute2(attributes, "interpret-as", element.interpretAs);
|
|
1204
|
+
addAttribute2(attributes, "format", element.format);
|
|
1205
|
+
addAttribute2(attributes, "detail", element.detail);
|
|
1206
|
+
break;
|
|
1207
|
+
case "phoneme":
|
|
1208
|
+
addAttribute2(attributes, "alphabet", element.alphabet);
|
|
1209
|
+
addAttribute2(attributes, "ph", element.ph);
|
|
1210
|
+
break;
|
|
1211
|
+
case "emphasis":
|
|
1212
|
+
addAttribute2(attributes, "level", element.level);
|
|
1213
|
+
break;
|
|
1214
|
+
case "audio":
|
|
1215
|
+
addAttribute2(attributes, "src", element.src);
|
|
1216
|
+
addAttribute2(attributes, "desc", element.desc);
|
|
1217
|
+
addAttribute2(attributes, "clipBegin", element.clipBegin);
|
|
1218
|
+
addAttribute2(attributes, "clipEnd", element.clipEnd);
|
|
1219
|
+
addAttribute2(attributes, "speed", element.speed);
|
|
1220
|
+
addAttribute2(attributes, "repeatCount", element.repeatCount);
|
|
1221
|
+
addAttribute2(attributes, "repeatDuration", element.repeatDuration);
|
|
1222
|
+
addAttribute2(attributes, "soundLevel", element.soundLevel);
|
|
1223
|
+
break;
|
|
1224
|
+
case "sub":
|
|
1225
|
+
addAttribute2(attributes, "alias", element.alias);
|
|
1226
|
+
break;
|
|
1227
|
+
case "lang":
|
|
1228
|
+
addAttribute2(attributes, "xml:lang", element.lang);
|
|
1229
|
+
break;
|
|
1230
|
+
case "mark":
|
|
1231
|
+
addAttribute2(attributes, "name", element.name);
|
|
1232
|
+
break;
|
|
1233
|
+
case "bookmark":
|
|
1234
|
+
addAttribute2(attributes, "mark", element.mark);
|
|
1235
|
+
break;
|
|
1236
|
+
case "lexicon":
|
|
1237
|
+
addAttribute2(attributes, "uri", element.uri);
|
|
1238
|
+
break;
|
|
1239
|
+
case "mstts:silence":
|
|
1240
|
+
case "silence":
|
|
1241
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.silenceType);
|
|
1242
|
+
addAttribute2(attributes, "value", element.value);
|
|
1243
|
+
break;
|
|
1244
|
+
case "mstts:viseme":
|
|
1245
|
+
case "viseme":
|
|
1246
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.visemeType);
|
|
1247
|
+
break;
|
|
1248
|
+
case "mstts:audioduration":
|
|
1249
|
+
addAttribute2(attributes, "value", element.value);
|
|
1250
|
+
break;
|
|
1251
|
+
case "mstts:turn":
|
|
1252
|
+
addAttribute2(attributes, "voice", element.voice);
|
|
1253
|
+
addAttribute2(attributes, "speaker", element.speaker);
|
|
1254
|
+
break;
|
|
1255
|
+
case "mstts:backgroundaudio":
|
|
1256
|
+
addAttribute2(attributes, "src", element.src);
|
|
1257
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1258
|
+
addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
|
|
1259
|
+
addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
|
|
1260
|
+
break;
|
|
1261
|
+
case "mstts:ttsembedding":
|
|
1262
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1263
|
+
break;
|
|
1264
|
+
case "mstts:embedding":
|
|
1265
|
+
addAttribute2(attributes, "id", element.id);
|
|
1266
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1267
|
+
break;
|
|
1268
|
+
case "mstts:voiceconversion":
|
|
1269
|
+
addAttribute2(attributes, "url", element.url);
|
|
1270
|
+
addAttribute2(attributes, "profile", element.profile);
|
|
1271
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
|
|
1275
|
+
}
|
|
1276
|
+
function childrenOf(node) {
|
|
1277
|
+
return node.children ?? [];
|
|
1278
|
+
}
|
|
1279
|
+
function extractSsmlTranslatableText(ssml, options = {}) {
|
|
1280
|
+
const document = parseSsml(ssml);
|
|
1281
|
+
const skipTags = new Set((options.skipTags ?? DEFAULT_TRANSLATION_SKIP_TAGS).map((tag) => tag.toLowerCase()));
|
|
1282
|
+
const result = [];
|
|
1283
|
+
const visit = (nodes, ancestors, path) => {
|
|
1284
|
+
nodes.forEach((node, index) => {
|
|
1285
|
+
if (typeof node === "string") {
|
|
1286
|
+
if (options.includeWhitespace || node.trim().length > 0) {
|
|
1287
|
+
const context = {
|
|
1288
|
+
ancestorTags: [...ancestors],
|
|
1289
|
+
parentAttributes: {},
|
|
1290
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1291
|
+
path: [...path, String(index)]
|
|
1292
|
+
};
|
|
1293
|
+
if (options.filter?.(context) ?? true) result.push(node);
|
|
1294
|
+
}
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (node.type === "text") {
|
|
1298
|
+
if (options.includeWhitespace || node.value.trim().length > 0) {
|
|
1299
|
+
const context = {
|
|
1300
|
+
ancestorTags: [...ancestors],
|
|
1301
|
+
parentAttributes: {},
|
|
1302
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1303
|
+
path: [...path, String(index)]
|
|
1304
|
+
};
|
|
1305
|
+
if (options.filter?.(context) ?? true) result.push(node.value);
|
|
1306
|
+
}
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const tag = elementName(node);
|
|
1310
|
+
if (skipTags.has(tag.toLowerCase())) return;
|
|
1311
|
+
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1312
|
+
});
|
|
1313
|
+
};
|
|
1314
|
+
visit(childrenOf(document), ["speak"], []);
|
|
1315
|
+
return result;
|
|
1316
|
+
}
|
|
1317
|
+
function splitSentences(text) {
|
|
1318
|
+
const sentences = [];
|
|
1319
|
+
let start = 0;
|
|
1320
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1321
|
+
const character = text[index];
|
|
1322
|
+
const isTerminator = "\u3002\uFF01\uFF1F!?".includes(character) || character === "." && /\s|$/.test(text[index + 1] ?? "");
|
|
1323
|
+
if (isTerminator) {
|
|
1324
|
+
const value = text.slice(start, index + 1).trim();
|
|
1325
|
+
if (value) sentences.push(value);
|
|
1326
|
+
start = index + 1;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const tail = text.slice(start).trim();
|
|
1330
|
+
if (tail) sentences.push(tail);
|
|
1331
|
+
return sentences;
|
|
1332
|
+
}
|
|
1333
|
+
function fromPlainTextToSsml(text, options = {}) {
|
|
1334
|
+
if (typeof text !== "string") throw new TypeError("Plain text must be a string");
|
|
1335
|
+
const paragraphs = text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
|
|
1336
|
+
const useSentences = options.splitSentences ?? options.includeSentences ?? true;
|
|
1337
|
+
const paragraphNodes = paragraphs.map((paragraph) => ({
|
|
1338
|
+
type: "p",
|
|
1339
|
+
children: useSentences ? splitSentences(paragraph).map((sentence) => ({ type: "s", children: [sentence] })) : [paragraph]
|
|
1340
|
+
}));
|
|
1341
|
+
const voiceName = options.voice ?? options.voiceName;
|
|
1342
|
+
const children = voiceName ? [{ type: "voice", name: voiceName, children: paragraphNodes }] : paragraphNodes;
|
|
1343
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1344
|
+
${serializeDocument2({
|
|
1345
|
+
version: options.version ?? "1.0",
|
|
1346
|
+
lang: options.lang ?? options.language ?? "en-US",
|
|
1347
|
+
children
|
|
1348
|
+
})}`;
|
|
1349
|
+
}
|
|
1350
|
+
function serializeDocument2(document) {
|
|
1351
|
+
const attributes = [`version="${document.version}"`, `xml:lang="${document.lang}"`];
|
|
1352
|
+
const serialize = (node) => {
|
|
1353
|
+
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1354
|
+
if (node.type === "text") return serialize(node.value);
|
|
1355
|
+
const tag = elementName(node);
|
|
1356
|
+
const nodeAttributes = elementAttributes(node);
|
|
1357
|
+
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1358
|
+
const children = childrenOf(node).map(serialize).join("");
|
|
1359
|
+
return children ? `<${tag}${serializedAttributes}>${children}</${tag}>` : `<${tag}${serializedAttributes}/>`;
|
|
1360
|
+
};
|
|
1361
|
+
return `<speak ${attributes.join(" ")} xmlns="http://www.w3.org/2001/10/synthesis">${(document.children ?? []).map(serialize).join("")}</speak>`;
|
|
1362
|
+
}
|
|
1363
|
+
function flatten(document) {
|
|
1364
|
+
const result = [];
|
|
1365
|
+
const visit = (nodes, path) => {
|
|
1366
|
+
nodes.forEach((node, index) => {
|
|
1367
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1368
|
+
const currentPath = `${path}/${index}`;
|
|
1369
|
+
result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
|
|
1370
|
+
visit(childrenOf(node), currentPath);
|
|
1371
|
+
});
|
|
1372
|
+
};
|
|
1373
|
+
result.push({ name: "speak", attributes: { version: document.version, "xml:lang": document.lang }, path: "0" });
|
|
1374
|
+
visit(childrenOf(document), "0");
|
|
1375
|
+
return result;
|
|
1376
|
+
}
|
|
1377
|
+
function validateSsmlStructureIntegrity(originalSsml, translatedSsml) {
|
|
1378
|
+
const mismatches = [];
|
|
1379
|
+
let original;
|
|
1380
|
+
let translated;
|
|
1381
|
+
try {
|
|
1382
|
+
original = parseSsml(originalSsml);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
mismatches.push({ kind: "parse", message: `Original SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1385
|
+
return {
|
|
1386
|
+
isValid: false,
|
|
1387
|
+
valid: false,
|
|
1388
|
+
errors: mismatches.map((item) => item.message),
|
|
1389
|
+
mismatches,
|
|
1390
|
+
mismatchedTags: []
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
try {
|
|
1394
|
+
translated = parseSsml(translatedSsml);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
mismatches.push({ kind: "parse", message: `Translated SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1397
|
+
return {
|
|
1398
|
+
isValid: false,
|
|
1399
|
+
valid: false,
|
|
1400
|
+
errors: mismatches.map((item) => item.message),
|
|
1401
|
+
mismatches,
|
|
1402
|
+
mismatchedTags: []
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const originalElements = flatten(original);
|
|
1406
|
+
const translatedElements = flatten(translated);
|
|
1407
|
+
const count = Math.max(originalElements.length, translatedElements.length);
|
|
1408
|
+
for (let index = 0; index < count; index += 1) {
|
|
1409
|
+
const originalElement = originalElements[index];
|
|
1410
|
+
const translatedElement = translatedElements[index];
|
|
1411
|
+
if (!originalElement || !translatedElement || originalElement.name !== translatedElement.name) {
|
|
1412
|
+
mismatches.push({
|
|
1413
|
+
kind: "element",
|
|
1414
|
+
message: `SSML element structure differs at index ${index}`,
|
|
1415
|
+
original: originalElement?.name,
|
|
1416
|
+
path: originalElement?.path ?? translatedElement?.path ?? String(index),
|
|
1417
|
+
translated: translatedElement?.name
|
|
1418
|
+
});
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const attributeNames = /* @__PURE__ */ new Set([
|
|
1422
|
+
...Object.keys(originalElement.attributes),
|
|
1423
|
+
...Object.keys(translatedElement.attributes)
|
|
1424
|
+
]);
|
|
1425
|
+
for (const attribute of attributeNames) {
|
|
1426
|
+
if (originalElement.attributes[attribute] !== translatedElement.attributes[attribute]) {
|
|
1427
|
+
mismatches.push({
|
|
1428
|
+
kind: "attribute",
|
|
1429
|
+
message: `Attribute ${attribute} differs on <${originalElement.name}>`,
|
|
1430
|
+
original: originalElement.attributes[attribute],
|
|
1431
|
+
path: originalElement.path,
|
|
1432
|
+
translated: translatedElement.attributes[attribute]
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const mismatchedTags = [
|
|
1438
|
+
...new Set(mismatches.flatMap((mismatch) => [mismatch.original, mismatch.translated].filter(Boolean)))
|
|
1439
|
+
];
|
|
1440
|
+
const errors = mismatches.map((mismatch) => mismatch.message);
|
|
1441
|
+
return { isValid: mismatches.length === 0, valid: mismatches.length === 0, errors, mismatches, mismatchedTags };
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1062
1444
|
// packages/ssml-core/src/generated/azureVoiceDefinitions.ts
|
|
1063
1445
|
var AZURE_VOICE_DEFINITIONS = [
|
|
1064
1446
|
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
@@ -1273,12 +1655,27 @@ function tokenizeElements(source) {
|
|
|
1273
1655
|
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1274
1656
|
}
|
|
1275
1657
|
const selfClosing = /\/\s*>$/.test(raw);
|
|
1658
|
+
const parent = openElements[openElements.length - 1];
|
|
1659
|
+
const childElementIndex = parent?.childElementCount;
|
|
1660
|
+
if (parent) parent.childElementCount += 1;
|
|
1276
1661
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1277
|
-
|
|
1662
|
+
const tokenName = nameMatch[1];
|
|
1663
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1664
|
+
tokens.push({
|
|
1665
|
+
attributes,
|
|
1666
|
+
childElementIndex,
|
|
1667
|
+
end,
|
|
1668
|
+
name: tokenName,
|
|
1669
|
+
parentName: parent?.name,
|
|
1670
|
+
parentVoiceName,
|
|
1671
|
+
selfClosing,
|
|
1672
|
+
start
|
|
1673
|
+
});
|
|
1278
1674
|
if (!selfClosing) {
|
|
1279
1675
|
openElements.push({
|
|
1280
|
-
|
|
1281
|
-
|
|
1676
|
+
childElementCount: 0,
|
|
1677
|
+
name: tokenName,
|
|
1678
|
+
voiceName: tokenVoiceName
|
|
1282
1679
|
});
|
|
1283
1680
|
}
|
|
1284
1681
|
index = end + 1;
|
|
@@ -1315,6 +1712,12 @@ function isValidAzureAudioDuration(value) {
|
|
|
1315
1712
|
if (!clock) return false;
|
|
1316
1713
|
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
1317
1714
|
}
|
|
1715
|
+
function isValidAzureBackgroundAudioDuration(value) {
|
|
1716
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
|
|
1717
|
+
if (!match) return false;
|
|
1718
|
+
const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
|
|
1719
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
|
|
1720
|
+
}
|
|
1318
1721
|
function attr(token, name) {
|
|
1319
1722
|
return token.attributes.get(name.toLowerCase());
|
|
1320
1723
|
}
|
|
@@ -1416,6 +1819,61 @@ function definitionMatchesLanguage(definition, voiceName, language, normalizeLan
|
|
|
1416
1819
|
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1417
1820
|
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1418
1821
|
}
|
|
1822
|
+
function canonicalTagName(name) {
|
|
1823
|
+
const normalized = name.toLowerCase();
|
|
1824
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1825
|
+
if (normalized === "sayas") return "say-as";
|
|
1826
|
+
return normalized;
|
|
1827
|
+
}
|
|
1828
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1829
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1830
|
+
return;
|
|
1831
|
+
const tagName = canonicalTagName(token.name);
|
|
1832
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1833
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1834
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1835
|
+
addDiagnostic(
|
|
1836
|
+
diagnostics,
|
|
1837
|
+
source,
|
|
1838
|
+
token.start,
|
|
1839
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1840
|
+
"error",
|
|
1841
|
+
"azure-unsupported-tag-for-voice"
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1846
|
+
const src = attr(token, "src");
|
|
1847
|
+
if (!src) {
|
|
1848
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
let parsed;
|
|
1852
|
+
try {
|
|
1853
|
+
parsed = new URL(src);
|
|
1854
|
+
} catch {
|
|
1855
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1859
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1860
|
+
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1861
|
+
try {
|
|
1862
|
+
return new URL(allowedOrigin).origin === parsed.origin;
|
|
1863
|
+
} catch {
|
|
1864
|
+
return allowedOrigin === parsed.origin;
|
|
1865
|
+
}
|
|
1866
|
+
}) ?? false;
|
|
1867
|
+
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
1868
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1869
|
+
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
1870
|
+
addDiagnostic(
|
|
1871
|
+
diagnostics,
|
|
1872
|
+
source,
|
|
1873
|
+
token.start,
|
|
1874
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1419
1877
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1420
1878
|
const name = token.name.toLowerCase();
|
|
1421
1879
|
if (name === "voice" && !attr(token, "name")?.trim())
|
|
@@ -1535,29 +1993,45 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
1535
1993
|
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1536
1994
|
}
|
|
1537
1995
|
if (name === "audio") {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1996
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1997
|
+
}
|
|
1998
|
+
if (name === "mstts:turn") {
|
|
1999
|
+
if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
|
|
2000
|
+
addDiagnostic(
|
|
2001
|
+
diagnostics,
|
|
2002
|
+
source,
|
|
2003
|
+
token.start,
|
|
2004
|
+
'<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
|
|
2005
|
+
);
|
|
2006
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
2007
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
2008
|
+
}
|
|
2009
|
+
if (name === "mstts:backgroundaudio") {
|
|
2010
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
2011
|
+
const volume = attr(token, "volume");
|
|
2012
|
+
if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
|
|
2013
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
2014
|
+
for (const [attribute, value] of [
|
|
2015
|
+
["fadein", attr(token, "fadein")],
|
|
2016
|
+
["fadeout", attr(token, "fadeout")]
|
|
2017
|
+
]) {
|
|
2018
|
+
if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
|
|
1553
2019
|
addDiagnostic(
|
|
1554
2020
|
diagnostics,
|
|
1555
2021
|
source,
|
|
1556
2022
|
token.start,
|
|
1557
|
-
`<
|
|
1558
|
-
"error"
|
|
2023
|
+
`<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
|
|
1559
2024
|
);
|
|
1560
2025
|
}
|
|
2026
|
+
if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
|
|
2027
|
+
addDiagnostic(
|
|
2028
|
+
diagnostics,
|
|
2029
|
+
source,
|
|
2030
|
+
token.start,
|
|
2031
|
+
"<mstts:backgroundaudio> must be the first element directly under <speak>."
|
|
2032
|
+
);
|
|
2033
|
+
if (!token.selfClosing)
|
|
2034
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1561
2035
|
}
|
|
1562
2036
|
}
|
|
1563
2037
|
function validateAzureSsml(ssml, options = {}) {
|
|
@@ -1587,6 +2061,16 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1587
2061
|
const tokens = tokenizeElements(ssml);
|
|
1588
2062
|
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
1589
2063
|
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
2064
|
+
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
2065
|
+
for (const [index, token] of backgroundAudioTokens.entries()) {
|
|
2066
|
+
if (index > 0)
|
|
2067
|
+
addDiagnostic(
|
|
2068
|
+
diagnostics,
|
|
2069
|
+
ssml,
|
|
2070
|
+
token.start,
|
|
2071
|
+
"An SSML document can contain at most one <mstts:backgroundaudio> element."
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
1590
2074
|
if (!speak || voices.length === 0)
|
|
1591
2075
|
addDiagnostic(
|
|
1592
2076
|
diagnostics,
|
|
@@ -1623,12 +2107,42 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1623
2107
|
);
|
|
1624
2108
|
}
|
|
1625
2109
|
for (const token of tokens) {
|
|
1626
|
-
const
|
|
2110
|
+
const tokenName = token.name.toLowerCase();
|
|
2111
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1627
2112
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2113
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2114
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2115
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2116
|
+
addDiagnostic(
|
|
2117
|
+
diagnostics,
|
|
2118
|
+
ssml,
|
|
2119
|
+
token.start,
|
|
2120
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
2121
|
+
"error",
|
|
2122
|
+
"azure-unsupported-model-for-voice"
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
1628
2125
|
}
|
|
1629
2126
|
return diagnostics;
|
|
1630
2127
|
}
|
|
1631
2128
|
|
|
2129
|
+
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2130
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
2131
|
+
apiVersion: "2025-10-01",
|
|
2132
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
2133
|
+
regions: [],
|
|
2134
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
// packages/ssml-core/src/voiceCatalog.ts
|
|
2138
|
+
function getAzureVoiceCatalogMetadata() {
|
|
2139
|
+
return {
|
|
2140
|
+
...AZURE_VOICE_CATALOG_METADATA,
|
|
2141
|
+
regions: [...AZURE_VOICE_CATALOG_METADATA.regions]
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
2145
|
+
|
|
1632
2146
|
// packages/azure-tts-client/src/errors.ts
|
|
1633
2147
|
var AzureTtsError = class extends Error {
|
|
1634
2148
|
constructor(status, statusText, responseBody, requestId) {
|
|
@@ -1819,12 +2333,17 @@ _options = new WeakMap();
|
|
|
1819
2333
|
buildPartialSsml,
|
|
1820
2334
|
buildSsml,
|
|
1821
2335
|
extractSsmlText,
|
|
2336
|
+
extractSsmlTranslatableText,
|
|
2337
|
+
fromPlainTextToSsml,
|
|
2338
|
+
getAzureVoiceCatalogMetadata,
|
|
2339
|
+
getBuiltInVoiceCatalogMetadata,
|
|
1822
2340
|
isValidAzureAudioDuration,
|
|
1823
2341
|
mapSsmlTextNodes,
|
|
1824
2342
|
normalizeAzureLanguage,
|
|
1825
2343
|
parseSsml,
|
|
1826
2344
|
synthesizeSpeech,
|
|
1827
2345
|
validateAzureSsml,
|
|
1828
|
-
validateSsml
|
|
2346
|
+
validateSsml,
|
|
2347
|
+
validateSsmlStructureIntegrity
|
|
1829
2348
|
});
|
|
1830
2349
|
//# sourceMappingURL=index.js.map
|