ssml-builder-js 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -31,11 +31,17 @@ __export(core_exports, {
31
31
  buildPartialSsml: () => buildPartialSsml,
32
32
  buildSsml: () => buildSsml,
33
33
  extractSsmlText: () => extractSsmlText,
34
+ extractSsmlTranslatableText: () => extractSsmlTranslatableText,
35
+ fromPlainTextToSsml: () => fromPlainTextToSsml,
36
+ getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
37
+ getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
38
+ isValidAzureAudioDuration: () => isValidAzureAudioDuration,
34
39
  mapSsmlTextNodes: () => mapSsmlTextNodes,
35
40
  normalizeAzureLanguage: () => normalizeAzureLanguage,
36
41
  parseSsml: () => parseSsml,
37
42
  validateAzureSsml: () => validateAzureSsml,
38
- validateSsml: () => validateSsml
43
+ validateSsml: () => validateSsml,
44
+ validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
39
45
  });
40
46
  module.exports = __toCommonJS(core_exports);
41
47
 
@@ -71,7 +77,14 @@ var SSML_TAGS = {
71
77
  MSTTS_SILENCE: "mstts:silence",
72
78
  SILENCE: "silence",
73
79
  MSTTS_VISEME: "mstts:viseme",
74
- VISEME: "viseme"
80
+ VISEME: "viseme",
81
+ MSTTS_AUDIO_DURATION: "mstts:audioduration",
82
+ MSTTS_DIALOG: "mstts:dialog",
83
+ MSTTS_TURN: "mstts:turn",
84
+ MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
85
+ MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
86
+ MSTTS_EMBEDDING: "mstts:embedding",
87
+ MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
75
88
  };
76
89
  var SSML_ATTRS = {
77
90
  VERSION: "version",
@@ -80,6 +93,7 @@ var SSML_ATTRS = {
80
93
  LANG: "lang",
81
94
  MSTTS_XMLNS: "xmlns:mstts",
82
95
  NAME: "name",
96
+ VOICE: "voice",
83
97
  EFFECT: "effect",
84
98
  RATE: "rate",
85
99
  PITCH: "pitch",
@@ -111,7 +125,9 @@ var SSML_ATTRS = {
111
125
  MARK: "mark",
112
126
  URI: "uri",
113
127
  TYPE: "type",
114
- VALUE: "value"
128
+ VALUE: "value",
129
+ FADE_IN: "fadein",
130
+ FADE_OUT: "fadeout"
115
131
  };
116
132
 
117
133
  // packages/ssml-core/src/builder.ts
@@ -200,6 +216,23 @@ function getAttributes(element) {
200
216
  case SSML_TAGS.VISEME:
201
217
  addAttribute(attributes, SSML_ATTRS.TYPE, element.typeValue ?? element.visemeType);
202
218
  break;
219
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
220
+ addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
221
+ break;
222
+ case SSML_TAGS.MSTTS_TURN:
223
+ addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
224
+ break;
225
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
226
+ addAttribute(attributes, SSML_ATTRS.SRC, element.src);
227
+ addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
228
+ addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
229
+ addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
230
+ break;
231
+ case SSML_TAGS.MSTTS_DIALOG:
232
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING:
233
+ case SSML_TAGS.MSTTS_EMBEDDING:
234
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION:
235
+ break;
203
236
  case SSML_TAGS.PARAGRAPH:
204
237
  case SSML_TAGS.SENTENCE:
205
238
  case SSML_TAGS.WORD:
@@ -224,6 +257,8 @@ function getTagName(element) {
224
257
  case SSML_TAGS.VISEME:
225
258
  case SSML_TAGS.MSTTS_VISEME:
226
259
  return SSML_TAGS.MSTTS_VISEME;
260
+ case SSML_TAGS.MSTTS_AUDIO_DURATION:
261
+ return SSML_TAGS.MSTTS_AUDIO_DURATION;
227
262
  case "element":
228
263
  case "custom":
229
264
  return element.name;
@@ -758,6 +793,46 @@ function convertElement(node) {
758
793
  if (typeValue !== void 0) element.typeValue = typeValue;
759
794
  return finishElement(element, node, attributes);
760
795
  }
796
+ case SSML_TAGS.MSTTS_AUDIO_DURATION: {
797
+ const element = { type: SSML_TAGS.MSTTS_AUDIO_DURATION };
798
+ const value = readAttribute(attributes, SSML_ATTRS.VALUE);
799
+ if (value !== void 0) element.value = value;
800
+ return finishElement(element, node, attributes);
801
+ }
802
+ case SSML_TAGS.MSTTS_DIALOG: {
803
+ const element = { type: SSML_TAGS.MSTTS_DIALOG };
804
+ return finishElement(element, node, attributes);
805
+ }
806
+ case SSML_TAGS.MSTTS_TURN: {
807
+ const element = { type: SSML_TAGS.MSTTS_TURN };
808
+ const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
809
+ if (voice !== void 0) element.voice = voice;
810
+ return finishElement(element, node, attributes);
811
+ }
812
+ case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
813
+ const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
814
+ const src = readAttribute(attributes, SSML_ATTRS.SRC);
815
+ const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
816
+ const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
817
+ const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
818
+ if (src !== void 0) element.src = src;
819
+ if (volume !== void 0) element.volume = volume;
820
+ if (fadeIn !== void 0) element.fadeIn = fadeIn;
821
+ if (fadeOut !== void 0) element.fadeOut = fadeOut;
822
+ return finishElement(element, node, attributes);
823
+ }
824
+ case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
825
+ const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
826
+ return finishElement(element, node, attributes);
827
+ }
828
+ case SSML_TAGS.MSTTS_EMBEDDING: {
829
+ const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
830
+ return finishElement(element, node, attributes);
831
+ }
832
+ case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
833
+ const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
834
+ return finishElement(element, node, attributes);
835
+ }
761
836
  default: {
762
837
  const element = {
763
838
  name: node.name,
@@ -1032,93 +1107,403 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
1032
1107
  return result + ssml.slice(cursor);
1033
1108
  }
1034
1109
 
1110
+ // packages/ssml-core/src/migration.ts
1111
+ var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
1112
+ function elementName(element) {
1113
+ switch (element.type) {
1114
+ case "custom":
1115
+ case "element":
1116
+ return element.name;
1117
+ case "expressAs":
1118
+ return "mstts:express-as";
1119
+ case "sayAs":
1120
+ return "say-as";
1121
+ case "silence":
1122
+ return "mstts:silence";
1123
+ case "viseme":
1124
+ return "mstts:viseme";
1125
+ default:
1126
+ return element.type;
1127
+ }
1128
+ }
1129
+ function addAttribute2(attributes, name, value) {
1130
+ if (value !== void 0) attributes[name] = value;
1131
+ }
1132
+ function elementAttributes(element) {
1133
+ const attributes = { ...element.attributes ?? {} };
1134
+ switch (element.type) {
1135
+ case "voice":
1136
+ addAttribute2(attributes, "name", element.name);
1137
+ addAttribute2(attributes, "effect", element.effect);
1138
+ break;
1139
+ case "prosody":
1140
+ addAttribute2(attributes, "rate", element.rate);
1141
+ addAttribute2(attributes, "pitch", element.pitch);
1142
+ addAttribute2(attributes, "volume", element.volume);
1143
+ addAttribute2(attributes, "contour", element.contour);
1144
+ addAttribute2(attributes, "range", element.range);
1145
+ break;
1146
+ case "break":
1147
+ addAttribute2(attributes, "time", element.time);
1148
+ addAttribute2(attributes, "strength", element.strength);
1149
+ break;
1150
+ case "express-as":
1151
+ case "expressAs":
1152
+ case "mstts:express-as":
1153
+ addAttribute2(attributes, "style", element.style);
1154
+ addAttribute2(attributes, "styledegree", element.styleDegree);
1155
+ addAttribute2(attributes, "role", element.role);
1156
+ break;
1157
+ case "say-as":
1158
+ case "sayAs":
1159
+ addAttribute2(attributes, "interpret-as", element.interpretAs);
1160
+ addAttribute2(attributes, "format", element.format);
1161
+ addAttribute2(attributes, "detail", element.detail);
1162
+ break;
1163
+ case "phoneme":
1164
+ addAttribute2(attributes, "alphabet", element.alphabet);
1165
+ addAttribute2(attributes, "ph", element.ph);
1166
+ break;
1167
+ case "emphasis":
1168
+ addAttribute2(attributes, "level", element.level);
1169
+ break;
1170
+ case "audio":
1171
+ addAttribute2(attributes, "src", element.src);
1172
+ addAttribute2(attributes, "desc", element.desc);
1173
+ addAttribute2(attributes, "clipBegin", element.clipBegin);
1174
+ addAttribute2(attributes, "clipEnd", element.clipEnd);
1175
+ addAttribute2(attributes, "speed", element.speed);
1176
+ addAttribute2(attributes, "repeatCount", element.repeatCount);
1177
+ addAttribute2(attributes, "repeatDuration", element.repeatDuration);
1178
+ addAttribute2(attributes, "soundLevel", element.soundLevel);
1179
+ break;
1180
+ case "sub":
1181
+ addAttribute2(attributes, "alias", element.alias);
1182
+ break;
1183
+ case "lang":
1184
+ addAttribute2(attributes, "xml:lang", element.lang);
1185
+ break;
1186
+ case "mark":
1187
+ addAttribute2(attributes, "name", element.name);
1188
+ break;
1189
+ case "bookmark":
1190
+ addAttribute2(attributes, "mark", element.mark);
1191
+ break;
1192
+ case "lexicon":
1193
+ addAttribute2(attributes, "uri", element.uri);
1194
+ break;
1195
+ case "mstts:silence":
1196
+ case "silence":
1197
+ addAttribute2(attributes, "type", element.typeValue ?? element.silenceType);
1198
+ addAttribute2(attributes, "value", element.value);
1199
+ break;
1200
+ case "mstts:viseme":
1201
+ case "viseme":
1202
+ addAttribute2(attributes, "type", element.typeValue ?? element.visemeType);
1203
+ break;
1204
+ case "mstts:audioduration":
1205
+ addAttribute2(attributes, "value", element.value);
1206
+ break;
1207
+ case "mstts:turn":
1208
+ addAttribute2(attributes, "voice", element.voice);
1209
+ break;
1210
+ case "mstts:backgroundaudio":
1211
+ addAttribute2(attributes, "src", element.src);
1212
+ addAttribute2(attributes, "volume", element.volume);
1213
+ addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
1214
+ addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
1215
+ break;
1216
+ }
1217
+ return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
1218
+ }
1219
+ function childrenOf(node) {
1220
+ return node.children ?? [];
1221
+ }
1222
+ function extractSsmlTranslatableText(ssml, options = {}) {
1223
+ const document = parseSsml(ssml);
1224
+ const skipTags = new Set((options.skipTags ?? DEFAULT_TRANSLATION_SKIP_TAGS).map((tag) => tag.toLowerCase()));
1225
+ const result = [];
1226
+ const visit = (nodes, ancestors, path) => {
1227
+ nodes.forEach((node, index) => {
1228
+ if (typeof node === "string") {
1229
+ if (options.includeWhitespace || node.trim().length > 0) {
1230
+ const context = {
1231
+ ancestorTags: [...ancestors],
1232
+ parentAttributes: {},
1233
+ parentTag: ancestors[ancestors.length - 1] ?? "",
1234
+ path: [...path, String(index)]
1235
+ };
1236
+ if (options.filter?.(context) ?? true) result.push(node);
1237
+ }
1238
+ return;
1239
+ }
1240
+ if (node.type === "text") {
1241
+ if (options.includeWhitespace || node.value.trim().length > 0) {
1242
+ const context = {
1243
+ ancestorTags: [...ancestors],
1244
+ parentAttributes: {},
1245
+ parentTag: ancestors[ancestors.length - 1] ?? "",
1246
+ path: [...path, String(index)]
1247
+ };
1248
+ if (options.filter?.(context) ?? true) result.push(node.value);
1249
+ }
1250
+ return;
1251
+ }
1252
+ const tag = elementName(node);
1253
+ if (skipTags.has(tag.toLowerCase())) return;
1254
+ visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
1255
+ });
1256
+ };
1257
+ visit(childrenOf(document), ["speak"], []);
1258
+ return result;
1259
+ }
1260
+ function splitSentences(text) {
1261
+ const sentences = [];
1262
+ let start = 0;
1263
+ for (let index = 0; index < text.length; index += 1) {
1264
+ const character = text[index];
1265
+ const isTerminator = "\u3002\uFF01\uFF1F!?".includes(character) || character === "." && /\s|$/.test(text[index + 1] ?? "");
1266
+ if (isTerminator) {
1267
+ const value = text.slice(start, index + 1).trim();
1268
+ if (value) sentences.push(value);
1269
+ start = index + 1;
1270
+ }
1271
+ }
1272
+ const tail = text.slice(start).trim();
1273
+ if (tail) sentences.push(tail);
1274
+ return sentences;
1275
+ }
1276
+ function fromPlainTextToSsml(text, options = {}) {
1277
+ if (typeof text !== "string") throw new TypeError("Plain text must be a string");
1278
+ const paragraphs = text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
1279
+ const useSentences = options.splitSentences ?? options.includeSentences ?? true;
1280
+ const paragraphNodes = paragraphs.map((paragraph) => ({
1281
+ type: "p",
1282
+ children: useSentences ? splitSentences(paragraph).map((sentence) => ({ type: "s", children: [sentence] })) : [paragraph]
1283
+ }));
1284
+ const voiceName = options.voice ?? options.voiceName;
1285
+ const children = voiceName ? [{ type: "voice", name: voiceName, children: paragraphNodes }] : paragraphNodes;
1286
+ return `<?xml version="1.0" encoding="UTF-8"?>
1287
+ ${serializeDocument2({
1288
+ version: options.version ?? "1.0",
1289
+ lang: options.lang ?? options.language ?? "en-US",
1290
+ children
1291
+ })}`;
1292
+ }
1293
+ function serializeDocument2(document) {
1294
+ const attributes = [`version="${document.version}"`, `xml:lang="${document.lang}"`];
1295
+ const serialize = (node) => {
1296
+ if (typeof node === "string") return node.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1297
+ if (node.type === "text") return serialize(node.value);
1298
+ const tag = elementName(node);
1299
+ const nodeAttributes = elementAttributes(node);
1300
+ const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, "&quot;")}"`).join("");
1301
+ const children = childrenOf(node).map(serialize).join("");
1302
+ return children ? `<${tag}${serializedAttributes}>${children}</${tag}>` : `<${tag}${serializedAttributes}/>`;
1303
+ };
1304
+ return `<speak ${attributes.join(" ")} xmlns="http://www.w3.org/2001/10/synthesis">${(document.children ?? []).map(serialize).join("")}</speak>`;
1305
+ }
1306
+ function flatten(document) {
1307
+ const result = [];
1308
+ const visit = (nodes, path) => {
1309
+ nodes.forEach((node, index) => {
1310
+ if (typeof node === "string" || node.type === "text") return;
1311
+ const currentPath = `${path}/${index}`;
1312
+ result.push({ name: elementName(node), attributes: elementAttributes(node), path: currentPath });
1313
+ visit(childrenOf(node), currentPath);
1314
+ });
1315
+ };
1316
+ result.push({ name: "speak", attributes: { version: document.version, "xml:lang": document.lang }, path: "0" });
1317
+ visit(childrenOf(document), "0");
1318
+ return result;
1319
+ }
1320
+ function validateSsmlStructureIntegrity(originalSsml, translatedSsml) {
1321
+ const mismatches = [];
1322
+ let original;
1323
+ let translated;
1324
+ try {
1325
+ original = parseSsml(originalSsml);
1326
+ } catch (error) {
1327
+ mismatches.push({ kind: "parse", message: `Original SSML cannot be parsed: ${String(error)}`, path: "0" });
1328
+ return {
1329
+ isValid: false,
1330
+ valid: false,
1331
+ errors: mismatches.map((item) => item.message),
1332
+ mismatches,
1333
+ mismatchedTags: []
1334
+ };
1335
+ }
1336
+ try {
1337
+ translated = parseSsml(translatedSsml);
1338
+ } catch (error) {
1339
+ mismatches.push({ kind: "parse", message: `Translated SSML cannot be parsed: ${String(error)}`, path: "0" });
1340
+ return {
1341
+ isValid: false,
1342
+ valid: false,
1343
+ errors: mismatches.map((item) => item.message),
1344
+ mismatches,
1345
+ mismatchedTags: []
1346
+ };
1347
+ }
1348
+ const originalElements = flatten(original);
1349
+ const translatedElements = flatten(translated);
1350
+ const count = Math.max(originalElements.length, translatedElements.length);
1351
+ for (let index = 0; index < count; index += 1) {
1352
+ const originalElement = originalElements[index];
1353
+ const translatedElement = translatedElements[index];
1354
+ if (!originalElement || !translatedElement || originalElement.name !== translatedElement.name) {
1355
+ mismatches.push({
1356
+ kind: "element",
1357
+ message: `SSML element structure differs at index ${index}`,
1358
+ original: originalElement?.name,
1359
+ path: originalElement?.path ?? translatedElement?.path ?? String(index),
1360
+ translated: translatedElement?.name
1361
+ });
1362
+ continue;
1363
+ }
1364
+ const attributeNames = /* @__PURE__ */ new Set([
1365
+ ...Object.keys(originalElement.attributes),
1366
+ ...Object.keys(translatedElement.attributes)
1367
+ ]);
1368
+ for (const attribute of attributeNames) {
1369
+ if (originalElement.attributes[attribute] !== translatedElement.attributes[attribute]) {
1370
+ mismatches.push({
1371
+ kind: "attribute",
1372
+ message: `Attribute ${attribute} differs on <${originalElement.name}>`,
1373
+ original: originalElement.attributes[attribute],
1374
+ path: originalElement.path,
1375
+ translated: translatedElement.attributes[attribute]
1376
+ });
1377
+ }
1378
+ }
1379
+ }
1380
+ const mismatchedTags = [
1381
+ ...new Set(mismatches.flatMap((mismatch) => [mismatch.original, mismatch.translated].filter(Boolean)))
1382
+ ];
1383
+ const errors = mismatches.map((mismatch) => mismatch.message);
1384
+ return { isValid: mismatches.length === 0, valid: mismatches.length === 0, errors, mismatches, mismatchedTags };
1385
+ }
1386
+
1387
+ // packages/ssml-core/src/generated/azureVoiceDefinitions.ts
1388
+ var AZURE_VOICE_DEFINITIONS = [
1389
+ { name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
1390
+ { name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
1391
+ { name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
1392
+ {
1393
+ name: "en-US-GuyNeural",
1394
+ locale: "en-US",
1395
+ styles: [
1396
+ "angry",
1397
+ "cheerful",
1398
+ "excited",
1399
+ "friendly",
1400
+ "hopeful",
1401
+ "newscast",
1402
+ "sad",
1403
+ "shouting",
1404
+ "terrified",
1405
+ "unfriendly",
1406
+ "whispering"
1407
+ ]
1408
+ },
1409
+ {
1410
+ name: "en-US-JennyMultilingualNeural",
1411
+ locale: "en-US",
1412
+ styles: [
1413
+ "cheerful",
1414
+ "empathetic",
1415
+ "excited",
1416
+ "friendly",
1417
+ "hopeful",
1418
+ "sad",
1419
+ "shouting",
1420
+ "terrified",
1421
+ "unfriendly",
1422
+ "whispering"
1423
+ ]
1424
+ },
1425
+ {
1426
+ name: "en-US-JennyNeural",
1427
+ locale: "en-US",
1428
+ styles: [
1429
+ "assistant",
1430
+ "chat",
1431
+ "customerservice",
1432
+ "newscast",
1433
+ "cheerful",
1434
+ "empathetic",
1435
+ "excited",
1436
+ "friendly",
1437
+ "hopeful",
1438
+ "sad",
1439
+ "shouting",
1440
+ "terrified",
1441
+ "unfriendly",
1442
+ "whispering"
1443
+ ]
1444
+ },
1445
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
1446
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1447
+ { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1448
+ { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1449
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
1450
+ { name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
1451
+ { name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
1452
+ { name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
1453
+ { name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
1454
+ { name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
1455
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" },
1456
+ { name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
1457
+ {
1458
+ name: "ru-RU-SvetlanaNeural",
1459
+ locale: "ru-RU",
1460
+ styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
1461
+ },
1462
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
1463
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
1464
+ {
1465
+ name: "zh-CN-XiaoxiaoNeural",
1466
+ locale: "zh-CN",
1467
+ styles: [
1468
+ "assistant",
1469
+ "chat",
1470
+ "customerservice",
1471
+ "newscast",
1472
+ "cheerful",
1473
+ "empathetic",
1474
+ "excited",
1475
+ "friendly",
1476
+ "hopeful",
1477
+ "sad",
1478
+ "terrified",
1479
+ "whispering",
1480
+ "poetry-reading",
1481
+ "sports_commentary",
1482
+ "sports_commentary_excited",
1483
+ "story"
1484
+ ]
1485
+ },
1486
+ {
1487
+ name: "zh-CN-YunxiNeural",
1488
+ locale: "zh-CN",
1489
+ styles: [
1490
+ "narration-relaxed",
1491
+ "embarrassed",
1492
+ "fearful",
1493
+ "sad",
1494
+ "disgruntled",
1495
+ "serious",
1496
+ "angry",
1497
+ "depressed",
1498
+ "chat",
1499
+ "cheerful",
1500
+ "assistant"
1501
+ ]
1502
+ },
1503
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
1504
+ ];
1505
+
1035
1506
  // packages/ssml-core/src/azureValidation.ts
1036
- var EXPRESS_AS_STYLES = {
1037
- "en-us-jennyneural": [
1038
- "assistant",
1039
- "chat",
1040
- "customerservice",
1041
- "newscast",
1042
- "cheerful",
1043
- "empathetic",
1044
- "excited",
1045
- "friendly",
1046
- "hopeful",
1047
- "sad",
1048
- "shouting",
1049
- "terrified",
1050
- "unfriendly",
1051
- "whispering"
1052
- ],
1053
- "en-us-guyneural": [
1054
- "angry",
1055
- "cheerful",
1056
- "excited",
1057
- "friendly",
1058
- "hopeful",
1059
- "newscast",
1060
- "sad",
1061
- "shouting",
1062
- "terrified",
1063
- "unfriendly",
1064
- "whispering"
1065
- ],
1066
- "en-us-jennymultilingualneural": [
1067
- "cheerful",
1068
- "empathetic",
1069
- "excited",
1070
- "friendly",
1071
- "hopeful",
1072
- "sad",
1073
- "shouting",
1074
- "terrified",
1075
- "unfriendly",
1076
- "whispering"
1077
- ],
1078
- "en-us-andrewneural": ["empathetic", "relieved"],
1079
- "ja-jp-mayuneural": ["calm", "cheerful", "sad"],
1080
- "ja-jp-nanamineural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
1081
- "ja-jp-keitaneural": ["chat"],
1082
- "ko-kr-sunhineural": ["cheerful", "sad"],
1083
- "zh-cn-yunxineural": [
1084
- "narration-relaxed",
1085
- "embarrassed",
1086
- "fearful",
1087
- "sad",
1088
- "disgruntled",
1089
- "serious",
1090
- "angry",
1091
- "depressed",
1092
- "chat",
1093
- "cheerful",
1094
- "assistant"
1095
- ],
1096
- "zh-cn-xiaoxiaoneural": [
1097
- "assistant",
1098
- "chat",
1099
- "customerservice",
1100
- "newscast",
1101
- "cheerful",
1102
- "empathetic",
1103
- "excited",
1104
- "friendly",
1105
- "hopeful",
1106
- "sad",
1107
- "terrified",
1108
- "whispering",
1109
- "poetry-reading",
1110
- "sports_commentary",
1111
- "sports_commentary_excited",
1112
- "story"
1113
- ],
1114
- "fr-fr-deniseneural": ["cheerful", "sad"],
1115
- "fr-fr-henrineural": ["cheerful", "sad"],
1116
- "pt-br-franciscaneural": ["calm"],
1117
- "it-it-elsaneural": ["cheerful", "sad"],
1118
- "de-de-katjaneural": ["cheerful", "sad"],
1119
- "de-de-conradneural": ["cheerful", "sad"],
1120
- "ru-ru-svetlananeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
1121
- };
1122
1507
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
1123
1508
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1124
1509
  "characters",
@@ -1213,12 +1598,23 @@ function tokenizeElements(source) {
1213
1598
  attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
1214
1599
  }
1215
1600
  const selfClosing = /\/\s*>$/.test(raw);
1601
+ const parent = openElements[openElements.length - 1];
1216
1602
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1217
- tokens.push({ attributes, end, name: nameMatch[1], parentVoiceName, selfClosing, start });
1603
+ const tokenName = nameMatch[1];
1604
+ const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1605
+ tokens.push({
1606
+ attributes,
1607
+ end,
1608
+ name: tokenName,
1609
+ parentName: parent?.name,
1610
+ parentVoiceName,
1611
+ selfClosing,
1612
+ start
1613
+ });
1218
1614
  if (!selfClosing) {
1219
1615
  openElements.push({
1220
- name: nameMatch[1],
1221
- voiceName: nameMatch[1].toLowerCase() === "voice" ? attributes.get("name") : parentVoiceName
1616
+ name: tokenName,
1617
+ voiceName: tokenVoiceName
1222
1618
  });
1223
1619
  }
1224
1620
  index = end + 1;
@@ -1247,18 +1643,17 @@ function isSupportedProsodyRate(value) {
1247
1643
  const numericValue = Number(multiplier[1]);
1248
1644
  return numericValue >= 0.5 && numericValue <= 2;
1249
1645
  }
1646
+ function isValidAzureAudioDuration(value) {
1647
+ const trimmed = value.trim();
1648
+ const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
1649
+ if (numeric) return Number(numeric[1]) > 0;
1650
+ const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
1651
+ if (!clock) return false;
1652
+ return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1653
+ }
1250
1654
  function attr(token, name) {
1251
1655
  return token.attributes.get(name.toLowerCase());
1252
1656
  }
1253
- var ADDITIONAL_VOICE_DEFINITIONS = [
1254
- { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" },
1255
- { name: "es-ES-ElviraNeural", locale: "es-ES" },
1256
- { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
1257
- { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1258
- { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
1259
- { name: "id-ID-GadisNeural", locale: "id-ID" },
1260
- { name: "ms-MY-YasminNeural", locale: "ms-MY" }
1261
- ];
1262
1657
  var DEFAULT_LANGUAGE_ALIASES = {
1263
1658
  "zh-CN": ["zh-Hans"],
1264
1659
  "zh-TW": ["zh-Hant"]
@@ -1321,10 +1716,7 @@ function definitionFromStyleMap(voiceName, styles) {
1321
1716
  }
1322
1717
  function normalizeVoiceCatalog(options) {
1323
1718
  const definitions = /* @__PURE__ */ new Map();
1324
- for (const [name, styles] of Object.entries(EXPRESS_AS_STYLES)) {
1325
- definitions.set(name.toLowerCase(), definitionFromStyleMap(name, styles));
1326
- }
1327
- for (const definition of ADDITIONAL_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
1719
+ for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
1328
1720
  for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
1329
1721
  for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
1330
1722
  for (const definition of options.customVoiceDefinitions ?? [])
@@ -1360,6 +1752,54 @@ function definitionMatchesLanguage(definition, voiceName, language, normalizeLan
1360
1752
  if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
1361
1753
  return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
1362
1754
  }
1755
+ function canonicalTagName(name) {
1756
+ const normalized = name.toLowerCase();
1757
+ if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
1758
+ if (normalized === "sayas") return "say-as";
1759
+ return normalized;
1760
+ }
1761
+ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
1762
+ if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
1763
+ return;
1764
+ const tagName = canonicalTagName(token.name);
1765
+ const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
1766
+ const supportedTags = definition.supportedTags?.map(canonicalTagName);
1767
+ if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
1768
+ addDiagnostic(
1769
+ diagnostics,
1770
+ source,
1771
+ token.start,
1772
+ `Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
1773
+ "error",
1774
+ "azure-unsupported-tag-for-voice"
1775
+ );
1776
+ }
1777
+ }
1778
+ function validateAudioSource(token, source, diagnostics, options, elementName2) {
1779
+ const src = attr(token, "src");
1780
+ if (!src) {
1781
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
1782
+ return;
1783
+ }
1784
+ let parsed;
1785
+ try {
1786
+ parsed = new URL(src);
1787
+ } catch {
1788
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1789
+ return;
1790
+ }
1791
+ if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1792
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1793
+ if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1794
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1795
+ else if (!options.allowExternalAudio)
1796
+ addDiagnostic(
1797
+ diagnostics,
1798
+ source,
1799
+ token.start,
1800
+ `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
1801
+ );
1802
+ }
1363
1803
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1364
1804
  const name = token.name.toLowerCase();
1365
1805
  if (name === "voice" && !attr(token, "name")?.trim())
@@ -1461,35 +1901,51 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1461
1901
  if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
1462
1902
  addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
1463
1903
  }
1904
+ if (name === "mstts:audioduration") {
1905
+ const value = attr(token, "value");
1906
+ if (!value || !isValidAzureAudioDuration(value))
1907
+ addDiagnostic(
1908
+ diagnostics,
1909
+ source,
1910
+ token.start,
1911
+ '<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
1912
+ );
1913
+ if (!token.selfClosing)
1914
+ addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
1915
+ }
1464
1916
  if (name === "mstts:viseme") {
1465
1917
  const type = attr(token, "type");
1466
1918
  if (!type || !ALLOWED_VISEME_TYPES.has(type))
1467
1919
  addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
1468
1920
  }
1469
1921
  if (name === "audio") {
1470
- const src = attr(token, "src");
1471
- if (!src) addDiagnostic(diagnostics, source, token.start, '<audio> requires a "src" attribute.');
1472
- else {
1473
- let parsed;
1474
- try {
1475
- parsed = new URL(src);
1476
- } catch {
1477
- addDiagnostic(diagnostics, source, token.start, "<audio src> must be an absolute HTTP(S) URL.");
1478
- return;
1479
- }
1480
- if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1481
- addDiagnostic(diagnostics, source, token.start, "<audio src> must use HTTPS.");
1482
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1483
- addDiagnostic(diagnostics, source, token.start, `<audio src> origin "${parsed.origin}" is not allowed.`);
1484
- else if (!options.allowExternalAudio)
1922
+ validateAudioSource(token, source, diagnostics, options, "audio");
1923
+ }
1924
+ if (name === "mstts:turn") {
1925
+ if (!attr(token, "voice")?.trim())
1926
+ addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1927
+ if (token.parentName?.toLowerCase() !== "mstts:dialog")
1928
+ addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1929
+ }
1930
+ if (name === "mstts:backgroundaudio") {
1931
+ validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1932
+ const volume = attr(token, "volume");
1933
+ if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
1934
+ addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1935
+ for (const [attribute, value] of [
1936
+ ["fadein", attr(token, "fadein")],
1937
+ ["fadeout", attr(token, "fadeout")]
1938
+ ]) {
1939
+ if (value && !isValidAzureAudioDuration(value))
1485
1940
  addDiagnostic(
1486
1941
  diagnostics,
1487
1942
  source,
1488
1943
  token.start,
1489
- `<audio src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`,
1490
- "error"
1944
+ `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
1491
1945
  );
1492
1946
  }
1947
+ if (!token.selfClosing)
1948
+ addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1493
1949
  }
1494
1950
  }
1495
1951
  function validateAzureSsml(ssml, options = {}) {
@@ -1555,21 +2011,57 @@ function validateAzureSsml(ssml, options = {}) {
1555
2011
  );
1556
2012
  }
1557
2013
  for (const token of tokens) {
1558
- const tokenVoiceName = options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2014
+ const tokenName = token.name.toLowerCase();
2015
+ const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
1559
2016
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2017
+ const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2018
+ validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2019
+ if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2020
+ addDiagnostic(
2021
+ diagnostics,
2022
+ ssml,
2023
+ token.start,
2024
+ `Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
2025
+ "error",
2026
+ "azure-unsupported-model-for-voice"
2027
+ );
2028
+ }
1560
2029
  }
1561
2030
  return diagnostics;
1562
2031
  }
2032
+
2033
+ // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2034
+ var AZURE_VOICE_CATALOG_METADATA = {
2035
+ apiVersion: "2025-10-01",
2036
+ generatedAt: "2026-08-28T00:00:00.000Z",
2037
+ regions: [],
2038
+ voiceCount: AZURE_VOICE_DEFINITIONS.length
2039
+ };
2040
+
2041
+ // packages/ssml-core/src/voiceCatalog.ts
2042
+ function getAzureVoiceCatalogMetadata() {
2043
+ return {
2044
+ ...AZURE_VOICE_CATALOG_METADATA,
2045
+ regions: [...AZURE_VOICE_CATALOG_METADATA.regions]
2046
+ };
2047
+ }
2048
+ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
1563
2049
  // Annotate the CommonJS export names for ESM import in node:
1564
2050
  0 && (module.exports = {
1565
2051
  areAzureLanguagesEquivalent,
1566
2052
  buildPartialSsml,
1567
2053
  buildSsml,
1568
2054
  extractSsmlText,
2055
+ extractSsmlTranslatableText,
2056
+ fromPlainTextToSsml,
2057
+ getAzureVoiceCatalogMetadata,
2058
+ getBuiltInVoiceCatalogMetadata,
2059
+ isValidAzureAudioDuration,
1569
2060
  mapSsmlTextNodes,
1570
2061
  normalizeAzureLanguage,
1571
2062
  parseSsml,
1572
2063
  validateAzureSsml,
1573
- validateSsml
2064
+ validateSsml,
2065
+ validateSsmlStructureIntegrity
1574
2066
  });
1575
2067
  //# sourceMappingURL=core.js.map