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
|
@@ -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,8 @@ var SSML_ATTRS = {
|
|
|
46
52
|
LANG: "lang",
|
|
47
53
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
48
54
|
NAME: "name",
|
|
55
|
+
VOICE: "voice",
|
|
56
|
+
SPEAKER: "speaker",
|
|
49
57
|
EFFECT: "effect",
|
|
50
58
|
RATE: "rate",
|
|
51
59
|
PITCH: "pitch",
|
|
@@ -76,8 +84,15 @@ var SSML_ATTRS = {
|
|
|
76
84
|
ALIAS: "alias",
|
|
77
85
|
MARK: "mark",
|
|
78
86
|
URI: "uri",
|
|
87
|
+
ID: "id",
|
|
88
|
+
MODEL: "model",
|
|
89
|
+
PROFILE: "profile",
|
|
90
|
+
URL: "url",
|
|
91
|
+
SPEAKER_PROFILE_ID: "speakerProfileId",
|
|
79
92
|
TYPE: "type",
|
|
80
|
-
VALUE: "value"
|
|
93
|
+
VALUE: "value",
|
|
94
|
+
FADE_IN: "fadein",
|
|
95
|
+
FADE_OUT: "fadeout"
|
|
81
96
|
};
|
|
82
97
|
|
|
83
98
|
// packages/ssml-core/src/builder.ts
|
|
@@ -169,6 +184,30 @@ function getAttributes(element) {
|
|
|
169
184
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
170
185
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
171
186
|
break;
|
|
187
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
188
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
189
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
|
|
190
|
+
break;
|
|
191
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
192
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
193
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
194
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
195
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
196
|
+
break;
|
|
197
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
198
|
+
break;
|
|
199
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
200
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
201
|
+
break;
|
|
202
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
203
|
+
addAttribute(attributes, SSML_ATTRS.ID, element.id);
|
|
204
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
205
|
+
break;
|
|
206
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
207
|
+
addAttribute(attributes, SSML_ATTRS.URL, element.url);
|
|
208
|
+
addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
|
|
209
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
210
|
+
break;
|
|
172
211
|
case SSML_TAGS.PARAGRAPH:
|
|
173
212
|
case SSML_TAGS.SENTENCE:
|
|
174
213
|
case SSML_TAGS.WORD:
|
|
@@ -735,6 +774,54 @@ function convertElement(node) {
|
|
|
735
774
|
if (value !== void 0) element.value = value;
|
|
736
775
|
return finishElement(element, node, attributes);
|
|
737
776
|
}
|
|
777
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
778
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
779
|
+
return finishElement(element, node, attributes);
|
|
780
|
+
}
|
|
781
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
782
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
783
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
784
|
+
const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
|
|
785
|
+
if (voice !== void 0) element.voice = voice;
|
|
786
|
+
if (speaker !== void 0) element.speaker = speaker;
|
|
787
|
+
return finishElement(element, node, attributes);
|
|
788
|
+
}
|
|
789
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
790
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
791
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
792
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
793
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
794
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
795
|
+
if (src !== void 0) element.src = src;
|
|
796
|
+
if (volume !== void 0) element.volume = volume;
|
|
797
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
798
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
799
|
+
return finishElement(element, node, attributes);
|
|
800
|
+
}
|
|
801
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
802
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
803
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
804
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
805
|
+
return finishElement(element, node, attributes);
|
|
806
|
+
}
|
|
807
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
808
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
809
|
+
const id = readAttribute(attributes, SSML_ATTRS.ID);
|
|
810
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
811
|
+
if (id !== void 0) element.id = id;
|
|
812
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
813
|
+
return finishElement(element, node, attributes);
|
|
814
|
+
}
|
|
815
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
816
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
817
|
+
const url = readAttribute(attributes, SSML_ATTRS.URL);
|
|
818
|
+
const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
|
|
819
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
820
|
+
if (url !== void 0) element.url = url;
|
|
821
|
+
if (profile !== void 0) element.profile = profile;
|
|
822
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
823
|
+
return finishElement(element, node, attributes);
|
|
824
|
+
}
|
|
738
825
|
default: {
|
|
739
826
|
const element = {
|
|
740
827
|
name: node.name,
|
|
@@ -1009,6 +1096,296 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1009
1096
|
return result + ssml.slice(cursor);
|
|
1010
1097
|
}
|
|
1011
1098
|
|
|
1099
|
+
// packages/ssml-core/src/migration.ts
|
|
1100
|
+
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1101
|
+
function elementName(element) {
|
|
1102
|
+
switch (element.type) {
|
|
1103
|
+
case "custom":
|
|
1104
|
+
case "element":
|
|
1105
|
+
return element.name;
|
|
1106
|
+
case "expressAs":
|
|
1107
|
+
return "mstts:express-as";
|
|
1108
|
+
case "sayAs":
|
|
1109
|
+
return "say-as";
|
|
1110
|
+
case "silence":
|
|
1111
|
+
return "mstts:silence";
|
|
1112
|
+
case "viseme":
|
|
1113
|
+
return "mstts:viseme";
|
|
1114
|
+
default:
|
|
1115
|
+
return element.type;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function addAttribute2(attributes, name, value) {
|
|
1119
|
+
if (value !== void 0) attributes[name] = value;
|
|
1120
|
+
}
|
|
1121
|
+
function elementAttributes(element) {
|
|
1122
|
+
const attributes = { ...element.attributes ?? {} };
|
|
1123
|
+
switch (element.type) {
|
|
1124
|
+
case "voice":
|
|
1125
|
+
addAttribute2(attributes, "name", element.name);
|
|
1126
|
+
addAttribute2(attributes, "effect", element.effect);
|
|
1127
|
+
break;
|
|
1128
|
+
case "prosody":
|
|
1129
|
+
addAttribute2(attributes, "rate", element.rate);
|
|
1130
|
+
addAttribute2(attributes, "pitch", element.pitch);
|
|
1131
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1132
|
+
addAttribute2(attributes, "contour", element.contour);
|
|
1133
|
+
addAttribute2(attributes, "range", element.range);
|
|
1134
|
+
break;
|
|
1135
|
+
case "break":
|
|
1136
|
+
addAttribute2(attributes, "time", element.time);
|
|
1137
|
+
addAttribute2(attributes, "strength", element.strength);
|
|
1138
|
+
break;
|
|
1139
|
+
case "express-as":
|
|
1140
|
+
case "expressAs":
|
|
1141
|
+
case "mstts:express-as":
|
|
1142
|
+
addAttribute2(attributes, "style", element.style);
|
|
1143
|
+
addAttribute2(attributes, "styledegree", element.styleDegree);
|
|
1144
|
+
addAttribute2(attributes, "role", element.role);
|
|
1145
|
+
break;
|
|
1146
|
+
case "say-as":
|
|
1147
|
+
case "sayAs":
|
|
1148
|
+
addAttribute2(attributes, "interpret-as", element.interpretAs);
|
|
1149
|
+
addAttribute2(attributes, "format", element.format);
|
|
1150
|
+
addAttribute2(attributes, "detail", element.detail);
|
|
1151
|
+
break;
|
|
1152
|
+
case "phoneme":
|
|
1153
|
+
addAttribute2(attributes, "alphabet", element.alphabet);
|
|
1154
|
+
addAttribute2(attributes, "ph", element.ph);
|
|
1155
|
+
break;
|
|
1156
|
+
case "emphasis":
|
|
1157
|
+
addAttribute2(attributes, "level", element.level);
|
|
1158
|
+
break;
|
|
1159
|
+
case "audio":
|
|
1160
|
+
addAttribute2(attributes, "src", element.src);
|
|
1161
|
+
addAttribute2(attributes, "desc", element.desc);
|
|
1162
|
+
addAttribute2(attributes, "clipBegin", element.clipBegin);
|
|
1163
|
+
addAttribute2(attributes, "clipEnd", element.clipEnd);
|
|
1164
|
+
addAttribute2(attributes, "speed", element.speed);
|
|
1165
|
+
addAttribute2(attributes, "repeatCount", element.repeatCount);
|
|
1166
|
+
addAttribute2(attributes, "repeatDuration", element.repeatDuration);
|
|
1167
|
+
addAttribute2(attributes, "soundLevel", element.soundLevel);
|
|
1168
|
+
break;
|
|
1169
|
+
case "sub":
|
|
1170
|
+
addAttribute2(attributes, "alias", element.alias);
|
|
1171
|
+
break;
|
|
1172
|
+
case "lang":
|
|
1173
|
+
addAttribute2(attributes, "xml:lang", element.lang);
|
|
1174
|
+
break;
|
|
1175
|
+
case "mark":
|
|
1176
|
+
addAttribute2(attributes, "name", element.name);
|
|
1177
|
+
break;
|
|
1178
|
+
case "bookmark":
|
|
1179
|
+
addAttribute2(attributes, "mark", element.mark);
|
|
1180
|
+
break;
|
|
1181
|
+
case "lexicon":
|
|
1182
|
+
addAttribute2(attributes, "uri", element.uri);
|
|
1183
|
+
break;
|
|
1184
|
+
case "mstts:silence":
|
|
1185
|
+
case "silence":
|
|
1186
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.silenceType);
|
|
1187
|
+
addAttribute2(attributes, "value", element.value);
|
|
1188
|
+
break;
|
|
1189
|
+
case "mstts:viseme":
|
|
1190
|
+
case "viseme":
|
|
1191
|
+
addAttribute2(attributes, "type", element.typeValue ?? element.visemeType);
|
|
1192
|
+
break;
|
|
1193
|
+
case "mstts:audioduration":
|
|
1194
|
+
addAttribute2(attributes, "value", element.value);
|
|
1195
|
+
break;
|
|
1196
|
+
case "mstts:turn":
|
|
1197
|
+
addAttribute2(attributes, "voice", element.voice);
|
|
1198
|
+
addAttribute2(attributes, "speaker", element.speaker);
|
|
1199
|
+
break;
|
|
1200
|
+
case "mstts:backgroundaudio":
|
|
1201
|
+
addAttribute2(attributes, "src", element.src);
|
|
1202
|
+
addAttribute2(attributes, "volume", element.volume);
|
|
1203
|
+
addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
|
|
1204
|
+
addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
|
|
1205
|
+
break;
|
|
1206
|
+
case "mstts:ttsembedding":
|
|
1207
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1208
|
+
break;
|
|
1209
|
+
case "mstts:embedding":
|
|
1210
|
+
addAttribute2(attributes, "id", element.id);
|
|
1211
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1212
|
+
break;
|
|
1213
|
+
case "mstts:voiceconversion":
|
|
1214
|
+
addAttribute2(attributes, "url", element.url);
|
|
1215
|
+
addAttribute2(attributes, "profile", element.profile);
|
|
1216
|
+
addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
|
|
1220
|
+
}
|
|
1221
|
+
function childrenOf(node) {
|
|
1222
|
+
return node.children ?? [];
|
|
1223
|
+
}
|
|
1224
|
+
function extractSsmlTranslatableText(ssml, options = {}) {
|
|
1225
|
+
const document = parseSsml(ssml);
|
|
1226
|
+
const skipTags = new Set((options.skipTags ?? DEFAULT_TRANSLATION_SKIP_TAGS).map((tag) => tag.toLowerCase()));
|
|
1227
|
+
const result = [];
|
|
1228
|
+
const visit = (nodes, ancestors, path) => {
|
|
1229
|
+
nodes.forEach((node, index) => {
|
|
1230
|
+
if (typeof node === "string") {
|
|
1231
|
+
if (options.includeWhitespace || node.trim().length > 0) {
|
|
1232
|
+
const context = {
|
|
1233
|
+
ancestorTags: [...ancestors],
|
|
1234
|
+
parentAttributes: {},
|
|
1235
|
+
parentTag: ancestors[ancestors.length - 1] ?? "",
|
|
1236
|
+
path: [...path, String(index)]
|
|
1237
|
+
};
|
|
1238
|
+
if (options.filter?.(context) ?? true) result.push(node);
|
|
1239
|
+
}
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (node.type === "text") {
|
|
1243
|
+
if (options.includeWhitespace || node.value.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.value);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const tag = elementName(node);
|
|
1255
|
+
if (skipTags.has(tag.toLowerCase())) return;
|
|
1256
|
+
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1257
|
+
});
|
|
1258
|
+
};
|
|
1259
|
+
visit(childrenOf(document), ["speak"], []);
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
1262
|
+
function splitSentences(text) {
|
|
1263
|
+
const sentences = [];
|
|
1264
|
+
let start = 0;
|
|
1265
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1266
|
+
const character = text[index];
|
|
1267
|
+
const isTerminator = "\u3002\uFF01\uFF1F!?".includes(character) || character === "." && /\s|$/.test(text[index + 1] ?? "");
|
|
1268
|
+
if (isTerminator) {
|
|
1269
|
+
const value = text.slice(start, index + 1).trim();
|
|
1270
|
+
if (value) sentences.push(value);
|
|
1271
|
+
start = index + 1;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const tail = text.slice(start).trim();
|
|
1275
|
+
if (tail) sentences.push(tail);
|
|
1276
|
+
return sentences;
|
|
1277
|
+
}
|
|
1278
|
+
function fromPlainTextToSsml(text, options = {}) {
|
|
1279
|
+
if (typeof text !== "string") throw new TypeError("Plain text must be a string");
|
|
1280
|
+
const paragraphs = text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
|
|
1281
|
+
const useSentences = options.splitSentences ?? options.includeSentences ?? true;
|
|
1282
|
+
const paragraphNodes = paragraphs.map((paragraph) => ({
|
|
1283
|
+
type: "p",
|
|
1284
|
+
children: useSentences ? splitSentences(paragraph).map((sentence) => ({ type: "s", children: [sentence] })) : [paragraph]
|
|
1285
|
+
}));
|
|
1286
|
+
const voiceName = options.voice ?? options.voiceName;
|
|
1287
|
+
const children = voiceName ? [{ type: "voice", name: voiceName, children: paragraphNodes }] : paragraphNodes;
|
|
1288
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1289
|
+
${serializeDocument2({
|
|
1290
|
+
version: options.version ?? "1.0",
|
|
1291
|
+
lang: options.lang ?? options.language ?? "en-US",
|
|
1292
|
+
children
|
|
1293
|
+
})}`;
|
|
1294
|
+
}
|
|
1295
|
+
function serializeDocument2(document) {
|
|
1296
|
+
const attributes = [`version="${document.version}"`, `xml:lang="${document.lang}"`];
|
|
1297
|
+
const serialize = (node) => {
|
|
1298
|
+
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1299
|
+
if (node.type === "text") return serialize(node.value);
|
|
1300
|
+
const tag = elementName(node);
|
|
1301
|
+
const nodeAttributes = elementAttributes(node);
|
|
1302
|
+
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1303
|
+
const children = childrenOf(node).map(serialize).join("");
|
|
1304
|
+
return children ? `<${tag}${serializedAttributes}>${children}</${tag}>` : `<${tag}${serializedAttributes}/>`;
|
|
1305
|
+
};
|
|
1306
|
+
return `<speak ${attributes.join(" ")} xmlns="http://www.w3.org/2001/10/synthesis">${(document.children ?? []).map(serialize).join("")}</speak>`;
|
|
1307
|
+
}
|
|
1308
|
+
function flatten(document) {
|
|
1309
|
+
const result = [];
|
|
1310
|
+
const visit = (nodes, path) => {
|
|
1311
|
+
nodes.forEach((node, index) => {
|
|
1312
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1313
|
+
const currentPath = `${path}/${index}`;
|
|
1314
|
+
result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
|
|
1315
|
+
visit(childrenOf(node), currentPath);
|
|
1316
|
+
});
|
|
1317
|
+
};
|
|
1318
|
+
result.push({ name: "speak", attributes: { version: document.version, "xml:lang": document.lang }, path: "0" });
|
|
1319
|
+
visit(childrenOf(document), "0");
|
|
1320
|
+
return result;
|
|
1321
|
+
}
|
|
1322
|
+
function validateSsmlStructureIntegrity(originalSsml, translatedSsml) {
|
|
1323
|
+
const mismatches = [];
|
|
1324
|
+
let original;
|
|
1325
|
+
let translated;
|
|
1326
|
+
try {
|
|
1327
|
+
original = parseSsml(originalSsml);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
mismatches.push({ kind: "parse", message: `Original SSML cannot be parsed: ${String(error)}`, path: "0" });
|
|
1330
|
+
return {
|
|
1331
|
+
isValid: false,
|
|
1332
|
+
valid: false,
|
|
1333
|
+
errors: mismatches.map((item) => item.message),
|
|
1334
|
+
mismatches,
|
|
1335
|
+
mismatchedTags: []
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
translated = parseSsml(translatedSsml);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
mismatches.push({ kind: "parse", message: `Translated 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
|
+
const originalElements = flatten(original);
|
|
1351
|
+
const translatedElements = flatten(translated);
|
|
1352
|
+
const count = Math.max(originalElements.length, translatedElements.length);
|
|
1353
|
+
for (let index = 0; index < count; index += 1) {
|
|
1354
|
+
const originalElement = originalElements[index];
|
|
1355
|
+
const translatedElement = translatedElements[index];
|
|
1356
|
+
if (!originalElement || !translatedElement || originalElement.name !== translatedElement.name) {
|
|
1357
|
+
mismatches.push({
|
|
1358
|
+
kind: "element",
|
|
1359
|
+
message: `SSML element structure differs at index ${index}`,
|
|
1360
|
+
original: originalElement?.name,
|
|
1361
|
+
path: originalElement?.path ?? translatedElement?.path ?? String(index),
|
|
1362
|
+
translated: translatedElement?.name
|
|
1363
|
+
});
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const attributeNames = /* @__PURE__ */ new Set([
|
|
1367
|
+
...Object.keys(originalElement.attributes),
|
|
1368
|
+
...Object.keys(translatedElement.attributes)
|
|
1369
|
+
]);
|
|
1370
|
+
for (const attribute of attributeNames) {
|
|
1371
|
+
if (originalElement.attributes[attribute] !== translatedElement.attributes[attribute]) {
|
|
1372
|
+
mismatches.push({
|
|
1373
|
+
kind: "attribute",
|
|
1374
|
+
message: `Attribute ${attribute} differs on <${originalElement.name}>`,
|
|
1375
|
+
original: originalElement.attributes[attribute],
|
|
1376
|
+
path: originalElement.path,
|
|
1377
|
+
translated: translatedElement.attributes[attribute]
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const mismatchedTags = [
|
|
1383
|
+
...new Set(mismatches.flatMap((mismatch) => [mismatch.original, mismatch.translated].filter(Boolean)))
|
|
1384
|
+
];
|
|
1385
|
+
const errors = mismatches.map((mismatch) => mismatch.message);
|
|
1386
|
+
return { isValid: mismatches.length === 0, valid: mismatches.length === 0, errors, mismatches, mismatchedTags };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1012
1389
|
// packages/ssml-core/src/generated/azureVoiceDefinitions.ts
|
|
1013
1390
|
var AZURE_VOICE_DEFINITIONS = [
|
|
1014
1391
|
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
@@ -1223,12 +1600,27 @@ function tokenizeElements(source) {
|
|
|
1223
1600
|
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1224
1601
|
}
|
|
1225
1602
|
const selfClosing = /\/\s*>$/.test(raw);
|
|
1603
|
+
const parent = openElements[openElements.length - 1];
|
|
1604
|
+
const childElementIndex = parent?.childElementCount;
|
|
1605
|
+
if (parent) parent.childElementCount += 1;
|
|
1226
1606
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1227
|
-
|
|
1607
|
+
const tokenName = nameMatch[1];
|
|
1608
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1609
|
+
tokens.push({
|
|
1610
|
+
attributes,
|
|
1611
|
+
childElementIndex,
|
|
1612
|
+
end,
|
|
1613
|
+
name: tokenName,
|
|
1614
|
+
parentName: parent?.name,
|
|
1615
|
+
parentVoiceName,
|
|
1616
|
+
selfClosing,
|
|
1617
|
+
start
|
|
1618
|
+
});
|
|
1228
1619
|
if (!selfClosing) {
|
|
1229
1620
|
openElements.push({
|
|
1230
|
-
|
|
1231
|
-
|
|
1621
|
+
childElementCount: 0,
|
|
1622
|
+
name: tokenName,
|
|
1623
|
+
voiceName: tokenVoiceName
|
|
1232
1624
|
});
|
|
1233
1625
|
}
|
|
1234
1626
|
index = end + 1;
|
|
@@ -1265,6 +1657,12 @@ function isValidAzureAudioDuration(value) {
|
|
|
1265
1657
|
if (!clock) return false;
|
|
1266
1658
|
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
1267
1659
|
}
|
|
1660
|
+
function isValidAzureBackgroundAudioDuration(value) {
|
|
1661
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
|
|
1662
|
+
if (!match) return false;
|
|
1663
|
+
const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
|
|
1664
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
|
|
1665
|
+
}
|
|
1268
1666
|
function attr(token, name) {
|
|
1269
1667
|
return token.attributes.get(name.toLowerCase());
|
|
1270
1668
|
}
|
|
@@ -1366,6 +1764,61 @@ function definitionMatchesLanguage(definition, voiceName, language, normalizeLan
|
|
|
1366
1764
|
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
1367
1765
|
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
1368
1766
|
}
|
|
1767
|
+
function canonicalTagName(name) {
|
|
1768
|
+
const normalized = name.toLowerCase();
|
|
1769
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
1770
|
+
if (normalized === "sayas") return "say-as";
|
|
1771
|
+
return normalized;
|
|
1772
|
+
}
|
|
1773
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
1774
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
1775
|
+
return;
|
|
1776
|
+
const tagName = canonicalTagName(token.name);
|
|
1777
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
1778
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
1779
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
1780
|
+
addDiagnostic(
|
|
1781
|
+
diagnostics,
|
|
1782
|
+
source,
|
|
1783
|
+
token.start,
|
|
1784
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
1785
|
+
"error",
|
|
1786
|
+
"azure-unsupported-tag-for-voice"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
1791
|
+
const src = attr(token, "src");
|
|
1792
|
+
if (!src) {
|
|
1793
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
let parsed;
|
|
1797
|
+
try {
|
|
1798
|
+
parsed = new URL(src);
|
|
1799
|
+
} catch {
|
|
1800
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1804
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1805
|
+
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1806
|
+
try {
|
|
1807
|
+
return new URL(allowedOrigin).origin === parsed.origin;
|
|
1808
|
+
} catch {
|
|
1809
|
+
return allowedOrigin === parsed.origin;
|
|
1810
|
+
}
|
|
1811
|
+
}) ?? false;
|
|
1812
|
+
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
1813
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
1814
|
+
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
1815
|
+
addDiagnostic(
|
|
1816
|
+
diagnostics,
|
|
1817
|
+
source,
|
|
1818
|
+
token.start,
|
|
1819
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1369
1822
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
1370
1823
|
const name = token.name.toLowerCase();
|
|
1371
1824
|
if (name === "voice" && !attr(token, "name")?.trim())
|
|
@@ -1485,29 +1938,45 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
1485
1938
|
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1486
1939
|
}
|
|
1487
1940
|
if (name === "audio") {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1941
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
1942
|
+
}
|
|
1943
|
+
if (name === "mstts:turn") {
|
|
1944
|
+
if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
|
|
1945
|
+
addDiagnostic(
|
|
1946
|
+
diagnostics,
|
|
1947
|
+
source,
|
|
1948
|
+
token.start,
|
|
1949
|
+
'<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
|
|
1950
|
+
);
|
|
1951
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
1952
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
1953
|
+
}
|
|
1954
|
+
if (name === "mstts:backgroundaudio") {
|
|
1955
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
1956
|
+
const volume = attr(token, "volume");
|
|
1957
|
+
if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
|
|
1958
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
1959
|
+
for (const [attribute, value] of [
|
|
1960
|
+
["fadein", attr(token, "fadein")],
|
|
1961
|
+
["fadeout", attr(token, "fadeout")]
|
|
1962
|
+
]) {
|
|
1963
|
+
if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
|
|
1503
1964
|
addDiagnostic(
|
|
1504
1965
|
diagnostics,
|
|
1505
1966
|
source,
|
|
1506
1967
|
token.start,
|
|
1507
|
-
`<
|
|
1508
|
-
"error"
|
|
1968
|
+
`<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
|
|
1509
1969
|
);
|
|
1510
1970
|
}
|
|
1971
|
+
if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
|
|
1972
|
+
addDiagnostic(
|
|
1973
|
+
diagnostics,
|
|
1974
|
+
source,
|
|
1975
|
+
token.start,
|
|
1976
|
+
"<mstts:backgroundaudio> must be the first element directly under <speak>."
|
|
1977
|
+
);
|
|
1978
|
+
if (!token.selfClosing)
|
|
1979
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
1511
1980
|
}
|
|
1512
1981
|
}
|
|
1513
1982
|
function validateAzureSsml(ssml, options = {}) {
|
|
@@ -1537,6 +2006,16 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1537
2006
|
const tokens = tokenizeElements(ssml);
|
|
1538
2007
|
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
1539
2008
|
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
2009
|
+
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
2010
|
+
for (const [index, token] of backgroundAudioTokens.entries()) {
|
|
2011
|
+
if (index > 0)
|
|
2012
|
+
addDiagnostic(
|
|
2013
|
+
diagnostics,
|
|
2014
|
+
ssml,
|
|
2015
|
+
token.start,
|
|
2016
|
+
"An SSML document can contain at most one <mstts:backgroundaudio> element."
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
1540
2019
|
if (!speak || voices.length === 0)
|
|
1541
2020
|
addDiagnostic(
|
|
1542
2021
|
diagnostics,
|
|
@@ -1573,12 +2052,42 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1573
2052
|
);
|
|
1574
2053
|
}
|
|
1575
2054
|
for (const token of tokens) {
|
|
1576
|
-
const
|
|
2055
|
+
const tokenName = token.name.toLowerCase();
|
|
2056
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1577
2057
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2058
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2059
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2060
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2061
|
+
addDiagnostic(
|
|
2062
|
+
diagnostics,
|
|
2063
|
+
ssml,
|
|
2064
|
+
token.start,
|
|
2065
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
2066
|
+
"error",
|
|
2067
|
+
"azure-unsupported-model-for-voice"
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
1578
2070
|
}
|
|
1579
2071
|
return diagnostics;
|
|
1580
2072
|
}
|
|
1581
2073
|
|
|
2074
|
+
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2075
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
2076
|
+
apiVersion: "2025-10-01",
|
|
2077
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
2078
|
+
regions: [],
|
|
2079
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// packages/ssml-core/src/voiceCatalog.ts
|
|
2083
|
+
function getAzureVoiceCatalogMetadata() {
|
|
2084
|
+
return {
|
|
2085
|
+
...AZURE_VOICE_CATALOG_METADATA,
|
|
2086
|
+
regions: [...AZURE_VOICE_CATALOG_METADATA.regions]
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
2090
|
+
|
|
1582
2091
|
export {
|
|
1583
2092
|
buildSsml,
|
|
1584
2093
|
parseSsml,
|
|
@@ -1586,9 +2095,14 @@ export {
|
|
|
1586
2095
|
validateSsml,
|
|
1587
2096
|
extractSsmlText,
|
|
1588
2097
|
mapSsmlTextNodes,
|
|
2098
|
+
extractSsmlTranslatableText,
|
|
2099
|
+
fromPlainTextToSsml,
|
|
2100
|
+
validateSsmlStructureIntegrity,
|
|
1589
2101
|
isValidAzureAudioDuration,
|
|
1590
2102
|
normalizeAzureLanguage,
|
|
1591
2103
|
areAzureLanguagesEquivalent,
|
|
1592
|
-
validateAzureSsml
|
|
2104
|
+
validateAzureSsml,
|
|
2105
|
+
getAzureVoiceCatalogMetadata,
|
|
2106
|
+
getBuiltInVoiceCatalogMetadata
|
|
1593
2107
|
};
|
|
1594
|
-
//# sourceMappingURL=chunk-
|
|
2108
|
+
//# sourceMappingURL=chunk-GW6CKHXF.mjs.map
|