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
|
@@ -959,6 +959,11 @@ var SSML_ATTRIBUTE_PRESETS = {
|
|
|
959
959
|
"mstts:audioduration": {
|
|
960
960
|
value: AUDIO_DURATION_PRESETS
|
|
961
961
|
},
|
|
962
|
+
"mstts:backgroundaudio": {
|
|
963
|
+
volume: PROSODY_VOLUME_PRESETS,
|
|
964
|
+
fadein: AUDIO_DURATION_PRESETS,
|
|
965
|
+
fadeout: AUDIO_DURATION_PRESETS
|
|
966
|
+
},
|
|
962
967
|
silence: {
|
|
963
968
|
type: SILENCE_TYPE_PRESETS,
|
|
964
969
|
value: SILENCE_VALUE_PRESETS
|
|
@@ -1343,7 +1348,13 @@ var SSML_TAGS = {
|
|
|
1343
1348
|
SILENCE: "silence",
|
|
1344
1349
|
MSTTS_VISEME: "mstts:viseme",
|
|
1345
1350
|
VISEME: "viseme",
|
|
1346
|
-
MSTTS_AUDIO_DURATION: "mstts:audioduration"
|
|
1351
|
+
MSTTS_AUDIO_DURATION: "mstts:audioduration",
|
|
1352
|
+
MSTTS_DIALOG: "mstts:dialog",
|
|
1353
|
+
MSTTS_TURN: "mstts:turn",
|
|
1354
|
+
MSTTS_BACKGROUND_AUDIO: "mstts:backgroundaudio",
|
|
1355
|
+
MSTTS_TTS_EMBEDDING: "mstts:ttsembedding",
|
|
1356
|
+
MSTTS_EMBEDDING: "mstts:embedding",
|
|
1357
|
+
MSTTS_VOICE_CONVERSION: "mstts:voiceconversion"
|
|
1347
1358
|
};
|
|
1348
1359
|
var SSML_ATTRS = {
|
|
1349
1360
|
VERSION: "version",
|
|
@@ -1352,6 +1363,8 @@ var SSML_ATTRS = {
|
|
|
1352
1363
|
LANG: "lang",
|
|
1353
1364
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
1354
1365
|
NAME: "name",
|
|
1366
|
+
VOICE: "voice",
|
|
1367
|
+
SPEAKER: "speaker",
|
|
1355
1368
|
EFFECT: "effect",
|
|
1356
1369
|
RATE: "rate",
|
|
1357
1370
|
PITCH: "pitch",
|
|
@@ -1382,8 +1395,15 @@ var SSML_ATTRS = {
|
|
|
1382
1395
|
ALIAS: "alias",
|
|
1383
1396
|
MARK: "mark",
|
|
1384
1397
|
URI: "uri",
|
|
1398
|
+
ID: "id",
|
|
1399
|
+
MODEL: "model",
|
|
1400
|
+
PROFILE: "profile",
|
|
1401
|
+
URL: "url",
|
|
1402
|
+
SPEAKER_PROFILE_ID: "speakerProfileId",
|
|
1385
1403
|
TYPE: "type",
|
|
1386
|
-
VALUE: "value"
|
|
1404
|
+
VALUE: "value",
|
|
1405
|
+
FADE_IN: "fadein",
|
|
1406
|
+
FADE_OUT: "fadeout"
|
|
1387
1407
|
};
|
|
1388
1408
|
function escapeText(value) {
|
|
1389
1409
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1473,6 +1493,30 @@ function getAttributes(element) {
|
|
|
1473
1493
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
1474
1494
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
1475
1495
|
break;
|
|
1496
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
1497
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
1498
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
|
|
1499
|
+
break;
|
|
1500
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
1501
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
1502
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
1503
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
1504
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
1505
|
+
break;
|
|
1506
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
1507
|
+
break;
|
|
1508
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
1509
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1510
|
+
break;
|
|
1511
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
1512
|
+
addAttribute(attributes, SSML_ATTRS.ID, element.id);
|
|
1513
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1514
|
+
break;
|
|
1515
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
1516
|
+
addAttribute(attributes, SSML_ATTRS.URL, element.url);
|
|
1517
|
+
addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
|
|
1518
|
+
addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
|
|
1519
|
+
break;
|
|
1476
1520
|
case SSML_TAGS.PARAGRAPH:
|
|
1477
1521
|
case SSML_TAGS.SENTENCE:
|
|
1478
1522
|
case SSML_TAGS.WORD:
|
|
@@ -2037,6 +2081,54 @@ function convertElement(node) {
|
|
|
2037
2081
|
if (value !== void 0) element.value = value;
|
|
2038
2082
|
return finishElement(element, node, attributes);
|
|
2039
2083
|
}
|
|
2084
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
2085
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
2086
|
+
return finishElement(element, node, attributes);
|
|
2087
|
+
}
|
|
2088
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
2089
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
2090
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
2091
|
+
const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
|
|
2092
|
+
if (voice !== void 0) element.voice = voice;
|
|
2093
|
+
if (speaker !== void 0) element.speaker = speaker;
|
|
2094
|
+
return finishElement(element, node, attributes);
|
|
2095
|
+
}
|
|
2096
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
2097
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
2098
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
2099
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
2100
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
2101
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
2102
|
+
if (src !== void 0) element.src = src;
|
|
2103
|
+
if (volume !== void 0) element.volume = volume;
|
|
2104
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
2105
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
2106
|
+
return finishElement(element, node, attributes);
|
|
2107
|
+
}
|
|
2108
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
2109
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
2110
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2111
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2112
|
+
return finishElement(element, node, attributes);
|
|
2113
|
+
}
|
|
2114
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
2115
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
2116
|
+
const id = readAttribute(attributes, SSML_ATTRS.ID);
|
|
2117
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2118
|
+
if (id !== void 0) element.id = id;
|
|
2119
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2120
|
+
return finishElement(element, node, attributes);
|
|
2121
|
+
}
|
|
2122
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
2123
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
2124
|
+
const url = readAttribute(attributes, SSML_ATTRS.URL);
|
|
2125
|
+
const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
|
|
2126
|
+
const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
|
|
2127
|
+
if (url !== void 0) element.url = url;
|
|
2128
|
+
if (profile !== void 0) element.profile = profile;
|
|
2129
|
+
if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
|
|
2130
|
+
return finishElement(element, node, attributes);
|
|
2131
|
+
}
|
|
2040
2132
|
default: {
|
|
2041
2133
|
const element = {
|
|
2042
2134
|
name: node.name,
|
|
@@ -2157,6 +2249,683 @@ function validateSsml(xmlString) {
|
|
|
2157
2249
|
};
|
|
2158
2250
|
}
|
|
2159
2251
|
}
|
|
2252
|
+
var AZURE_VOICE_DEFINITIONS = [
|
|
2253
|
+
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2254
|
+
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2255
|
+
{ name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
|
|
2256
|
+
{
|
|
2257
|
+
name: "en-US-GuyNeural",
|
|
2258
|
+
locale: "en-US",
|
|
2259
|
+
styles: [
|
|
2260
|
+
"angry",
|
|
2261
|
+
"cheerful",
|
|
2262
|
+
"excited",
|
|
2263
|
+
"friendly",
|
|
2264
|
+
"hopeful",
|
|
2265
|
+
"newscast",
|
|
2266
|
+
"sad",
|
|
2267
|
+
"shouting",
|
|
2268
|
+
"terrified",
|
|
2269
|
+
"unfriendly",
|
|
2270
|
+
"whispering"
|
|
2271
|
+
]
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
name: "en-US-JennyMultilingualNeural",
|
|
2275
|
+
locale: "en-US",
|
|
2276
|
+
styles: [
|
|
2277
|
+
"cheerful",
|
|
2278
|
+
"empathetic",
|
|
2279
|
+
"excited",
|
|
2280
|
+
"friendly",
|
|
2281
|
+
"hopeful",
|
|
2282
|
+
"sad",
|
|
2283
|
+
"shouting",
|
|
2284
|
+
"terrified",
|
|
2285
|
+
"unfriendly",
|
|
2286
|
+
"whispering"
|
|
2287
|
+
]
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
name: "en-US-JennyNeural",
|
|
2291
|
+
locale: "en-US",
|
|
2292
|
+
styles: [
|
|
2293
|
+
"assistant",
|
|
2294
|
+
"chat",
|
|
2295
|
+
"customerservice",
|
|
2296
|
+
"newscast",
|
|
2297
|
+
"cheerful",
|
|
2298
|
+
"empathetic",
|
|
2299
|
+
"excited",
|
|
2300
|
+
"friendly",
|
|
2301
|
+
"hopeful",
|
|
2302
|
+
"sad",
|
|
2303
|
+
"shouting",
|
|
2304
|
+
"terrified",
|
|
2305
|
+
"unfriendly",
|
|
2306
|
+
"whispering"
|
|
2307
|
+
]
|
|
2308
|
+
},
|
|
2309
|
+
{ name: "es-ES-ElviraNeural", locale: "es-ES" },
|
|
2310
|
+
{ name: "fil-PH-AngeloNeural", locale: "fil-PH" },
|
|
2311
|
+
{ name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2312
|
+
{ name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2313
|
+
{ name: "id-ID-GadisNeural", locale: "id-ID" },
|
|
2314
|
+
{ name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
|
|
2315
|
+
{ name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
|
|
2316
|
+
{ name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
|
|
2317
|
+
{ name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
|
|
2318
|
+
{ name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
|
|
2319
|
+
{ name: "ms-MY-YasminNeural", locale: "ms-MY" },
|
|
2320
|
+
{ name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
|
|
2321
|
+
{
|
|
2322
|
+
name: "ru-RU-SvetlanaNeural",
|
|
2323
|
+
locale: "ru-RU",
|
|
2324
|
+
styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
2325
|
+
},
|
|
2326
|
+
{ name: "th-TH-PremwadeeNeural", locale: "th-TH" },
|
|
2327
|
+
{ name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
|
|
2328
|
+
{
|
|
2329
|
+
name: "zh-CN-XiaoxiaoNeural",
|
|
2330
|
+
locale: "zh-CN",
|
|
2331
|
+
styles: [
|
|
2332
|
+
"assistant",
|
|
2333
|
+
"chat",
|
|
2334
|
+
"customerservice",
|
|
2335
|
+
"newscast",
|
|
2336
|
+
"cheerful",
|
|
2337
|
+
"empathetic",
|
|
2338
|
+
"excited",
|
|
2339
|
+
"friendly",
|
|
2340
|
+
"hopeful",
|
|
2341
|
+
"sad",
|
|
2342
|
+
"terrified",
|
|
2343
|
+
"whispering",
|
|
2344
|
+
"poetry-reading",
|
|
2345
|
+
"sports_commentary",
|
|
2346
|
+
"sports_commentary_excited",
|
|
2347
|
+
"story"
|
|
2348
|
+
]
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
name: "zh-CN-YunxiNeural",
|
|
2352
|
+
locale: "zh-CN",
|
|
2353
|
+
styles: [
|
|
2354
|
+
"narration-relaxed",
|
|
2355
|
+
"embarrassed",
|
|
2356
|
+
"fearful",
|
|
2357
|
+
"sad",
|
|
2358
|
+
"disgruntled",
|
|
2359
|
+
"serious",
|
|
2360
|
+
"angry",
|
|
2361
|
+
"depressed",
|
|
2362
|
+
"chat",
|
|
2363
|
+
"cheerful",
|
|
2364
|
+
"assistant"
|
|
2365
|
+
]
|
|
2366
|
+
},
|
|
2367
|
+
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
2368
|
+
];
|
|
2369
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
2370
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
2371
|
+
"characters",
|
|
2372
|
+
"spell-out",
|
|
2373
|
+
"cardinal",
|
|
2374
|
+
"ordinal",
|
|
2375
|
+
"number",
|
|
2376
|
+
"date",
|
|
2377
|
+
"time",
|
|
2378
|
+
"telephone",
|
|
2379
|
+
"fraction",
|
|
2380
|
+
"address",
|
|
2381
|
+
"name",
|
|
2382
|
+
"currency",
|
|
2383
|
+
"number_digit"
|
|
2384
|
+
]);
|
|
2385
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
2386
|
+
"Girl",
|
|
2387
|
+
"Boy",
|
|
2388
|
+
"YoungAdultFemale",
|
|
2389
|
+
"YoungAdultMale",
|
|
2390
|
+
"OlderAdultFemale",
|
|
2391
|
+
"OlderAdultMale",
|
|
2392
|
+
"SeniorFemale",
|
|
2393
|
+
"SeniorMale"
|
|
2394
|
+
]);
|
|
2395
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
2396
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
2397
|
+
"Leading",
|
|
2398
|
+
"Tailing",
|
|
2399
|
+
"Sentenceboundary",
|
|
2400
|
+
"Comma",
|
|
2401
|
+
"Semicolon",
|
|
2402
|
+
"Enumerationcomma"
|
|
2403
|
+
]);
|
|
2404
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
2405
|
+
function decodeAttribute(value) {
|
|
2406
|
+
return value.replace(
|
|
2407
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
2408
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
function findTagEnd2(source, start) {
|
|
2412
|
+
let quote = "";
|
|
2413
|
+
for (let index = start; index < source.length; index += 1) {
|
|
2414
|
+
const character = source[index];
|
|
2415
|
+
if (quote) {
|
|
2416
|
+
if (character === quote) quote = "";
|
|
2417
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
2418
|
+
else if (character === ">") return index;
|
|
2419
|
+
}
|
|
2420
|
+
return source.length - 1;
|
|
2421
|
+
}
|
|
2422
|
+
function tokenizeElements(source) {
|
|
2423
|
+
const tokens = [];
|
|
2424
|
+
const openElements = [];
|
|
2425
|
+
let index = 0;
|
|
2426
|
+
while (index < source.length) {
|
|
2427
|
+
const start = source.indexOf("<", index);
|
|
2428
|
+
if (start === -1) break;
|
|
2429
|
+
if (source.startsWith("<!--", start)) {
|
|
2430
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
2431
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
2435
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
2436
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (source.startsWith("<?", start)) {
|
|
2440
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
2441
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
const end = findTagEnd2(source, start + 1);
|
|
2445
|
+
const raw = source.slice(start, end + 1);
|
|
2446
|
+
if (raw.startsWith("</")) {
|
|
2447
|
+
openElements.pop();
|
|
2448
|
+
index = end + 1;
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
2452
|
+
if (!nameMatch?.[1]) {
|
|
2453
|
+
index = end + 1;
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
2457
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
2458
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
2459
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
2460
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
2461
|
+
}
|
|
2462
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
2463
|
+
const parent = openElements[openElements.length - 1];
|
|
2464
|
+
const childElementIndex = parent?.childElementCount;
|
|
2465
|
+
if (parent) parent.childElementCount += 1;
|
|
2466
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
2467
|
+
const tokenName = nameMatch[1];
|
|
2468
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
2469
|
+
tokens.push({
|
|
2470
|
+
attributes,
|
|
2471
|
+
childElementIndex,
|
|
2472
|
+
end,
|
|
2473
|
+
name: tokenName,
|
|
2474
|
+
parentName: parent?.name,
|
|
2475
|
+
parentVoiceName,
|
|
2476
|
+
selfClosing,
|
|
2477
|
+
start
|
|
2478
|
+
});
|
|
2479
|
+
if (!selfClosing) {
|
|
2480
|
+
openElements.push({
|
|
2481
|
+
childElementCount: 0,
|
|
2482
|
+
name: tokenName,
|
|
2483
|
+
voiceName: tokenVoiceName
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
index = end + 1;
|
|
2487
|
+
}
|
|
2488
|
+
return tokens;
|
|
2489
|
+
}
|
|
2490
|
+
function location(source, offset) {
|
|
2491
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
2492
|
+
const line = before.split("\n").length;
|
|
2493
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
2494
|
+
}
|
|
2495
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
|
|
2496
|
+
diagnostics.push({
|
|
2497
|
+
...location(source, offset),
|
|
2498
|
+
message,
|
|
2499
|
+
severity,
|
|
2500
|
+
source: "ssml-static-validator",
|
|
2501
|
+
...code2 ? { code: code2 } : {}
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function isSupportedProsodyRate(value) {
|
|
2505
|
+
const trimmed = value.trim();
|
|
2506
|
+
if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
|
|
2507
|
+
const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
|
|
2508
|
+
if (!multiplier) return false;
|
|
2509
|
+
const numericValue = Number(multiplier[1]);
|
|
2510
|
+
return numericValue >= 0.5 && numericValue <= 2;
|
|
2511
|
+
}
|
|
2512
|
+
function isValidAzureAudioDuration(value) {
|
|
2513
|
+
const trimmed = value.trim();
|
|
2514
|
+
const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
|
|
2515
|
+
if (numeric) return Number(numeric[1]) > 0;
|
|
2516
|
+
const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
|
|
2517
|
+
if (!clock) return false;
|
|
2518
|
+
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
2519
|
+
}
|
|
2520
|
+
function isValidAzureBackgroundAudioDuration(value) {
|
|
2521
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
|
|
2522
|
+
if (!match) return false;
|
|
2523
|
+
const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
|
|
2524
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
|
|
2525
|
+
}
|
|
2526
|
+
function attr(token, name) {
|
|
2527
|
+
return token.attributes.get(name.toLowerCase());
|
|
2528
|
+
}
|
|
2529
|
+
var DEFAULT_LANGUAGE_ALIASES = {
|
|
2530
|
+
"zh-CN": ["zh-Hans"],
|
|
2531
|
+
"zh-TW": ["zh-Hant"]
|
|
2532
|
+
};
|
|
2533
|
+
function canonicalLanguageTag(language) {
|
|
2534
|
+
const trimmed = language.trim();
|
|
2535
|
+
if (!trimmed) return "";
|
|
2536
|
+
try {
|
|
2537
|
+
return new Intl.Locale(trimmed).toString().toLowerCase();
|
|
2538
|
+
} catch {
|
|
2539
|
+
return trimmed.toLowerCase();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function createLanguageNormalizer(options) {
|
|
2543
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2544
|
+
const addAliasGroup = (canonical, values) => {
|
|
2545
|
+
const normalizedCanonical = canonicalLanguageTag(canonical);
|
|
2546
|
+
if (!normalizedCanonical) return;
|
|
2547
|
+
aliases.set(normalizedCanonical, normalizedCanonical);
|
|
2548
|
+
for (const value of values) {
|
|
2549
|
+
const normalizedValue = canonicalLanguageTag(value);
|
|
2550
|
+
if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
|
|
2554
|
+
for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
|
|
2555
|
+
addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
|
|
2556
|
+
return (language) => {
|
|
2557
|
+
const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
|
|
2558
|
+
const normalized = canonicalLanguageTag(customValue);
|
|
2559
|
+
return aliases.get(normalized) ?? normalized;
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
function voiceLocalePrefix(voiceName) {
|
|
2563
|
+
const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
|
|
2564
|
+
if (!match?.groups) return void 0;
|
|
2565
|
+
const tag = `${match.groups.language}-${match.groups.region}`;
|
|
2566
|
+
return {
|
|
2567
|
+
language: match.groups.language.toLowerCase(),
|
|
2568
|
+
region: match.groups.region.toLowerCase(),
|
|
2569
|
+
tag
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
function definitionFromStyleMap(voiceName, styles) {
|
|
2573
|
+
return {
|
|
2574
|
+
name: voiceName,
|
|
2575
|
+
locale: voiceLocalePrefix(voiceName)?.tag ?? "",
|
|
2576
|
+
styles
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
function normalizeVoiceCatalog(options) {
|
|
2580
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2581
|
+
for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
|
|
2582
|
+
for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2583
|
+
for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2584
|
+
for (const definition of options.customVoiceDefinitions ?? [])
|
|
2585
|
+
definitions.set(definition.name.toLowerCase(), definition);
|
|
2586
|
+
for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
|
|
2587
|
+
const key = voiceName.toLowerCase();
|
|
2588
|
+
const current = definitions.get(key);
|
|
2589
|
+
definitions.set(key, {
|
|
2590
|
+
...current ?? definitionFromStyleMap(voiceName, styles),
|
|
2591
|
+
name: current?.name ?? voiceName,
|
|
2592
|
+
styles: styles.map((style) => style.toLowerCase())
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
return definitions;
|
|
2596
|
+
}
|
|
2597
|
+
function diagnosticSeverity(policy) {
|
|
2598
|
+
if (policy === "ignore") return void 0;
|
|
2599
|
+
return policy === "error" ? "error" : "warning";
|
|
2600
|
+
}
|
|
2601
|
+
function languagePart(language) {
|
|
2602
|
+
try {
|
|
2603
|
+
return new Intl.Locale(language).language.toLowerCase();
|
|
2604
|
+
} catch {
|
|
2605
|
+
return language.split("-")[0]?.toLowerCase() ?? "";
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
|
|
2609
|
+
const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
|
|
2610
|
+
if (candidateLanguages.length === 0 || !language.trim()) return void 0;
|
|
2611
|
+
const normalizedLanguage = normalizeLanguage(language);
|
|
2612
|
+
const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
|
|
2613
|
+
if (normalizedCandidates.includes(normalizedLanguage)) return true;
|
|
2614
|
+
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
2615
|
+
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
2616
|
+
}
|
|
2617
|
+
function canonicalTagName(name) {
|
|
2618
|
+
const normalized = name.toLowerCase();
|
|
2619
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
2620
|
+
if (normalized === "sayas") return "say-as";
|
|
2621
|
+
return normalized;
|
|
2622
|
+
}
|
|
2623
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
2624
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
2625
|
+
return;
|
|
2626
|
+
const tagName = canonicalTagName(token.name);
|
|
2627
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
2628
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
2629
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
2630
|
+
addDiagnostic(
|
|
2631
|
+
diagnostics,
|
|
2632
|
+
source,
|
|
2633
|
+
token.start,
|
|
2634
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
2635
|
+
"error",
|
|
2636
|
+
"azure-unsupported-tag-for-voice"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
2641
|
+
const src = attr(token, "src");
|
|
2642
|
+
if (!src) {
|
|
2643
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
let parsed;
|
|
2647
|
+
try {
|
|
2648
|
+
parsed = new URL(src);
|
|
2649
|
+
} catch {
|
|
2650
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2654
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
2655
|
+
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2656
|
+
try {
|
|
2657
|
+
return new URL(allowedOrigin).origin === parsed.origin;
|
|
2658
|
+
} catch {
|
|
2659
|
+
return allowedOrigin === parsed.origin;
|
|
2660
|
+
}
|
|
2661
|
+
}) ?? false;
|
|
2662
|
+
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2663
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
2664
|
+
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2665
|
+
addDiagnostic(
|
|
2666
|
+
diagnostics,
|
|
2667
|
+
source,
|
|
2668
|
+
token.start,
|
|
2669
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
2673
|
+
const name = token.name.toLowerCase();
|
|
2674
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
2675
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
2676
|
+
if (name === "break") {
|
|
2677
|
+
const time = attr(token, "time");
|
|
2678
|
+
const strength = attr(token, "strength");
|
|
2679
|
+
if (!time && !strength)
|
|
2680
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
2681
|
+
if (time && strength)
|
|
2682
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
2683
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
2684
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
2685
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
2686
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
2687
|
+
}
|
|
2688
|
+
if (name === "prosody") {
|
|
2689
|
+
const rate = attr(token, "rate");
|
|
2690
|
+
const pitch = attr(token, "pitch");
|
|
2691
|
+
const volume = attr(token, "volume");
|
|
2692
|
+
if (rate && !isSupportedProsodyRate(rate))
|
|
2693
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
2694
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
2695
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
2696
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
2697
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
2698
|
+
}
|
|
2699
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
2700
|
+
const style = attr(token, "style");
|
|
2701
|
+
if (!style?.trim())
|
|
2702
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
2703
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
2704
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
2705
|
+
addDiagnostic(
|
|
2706
|
+
diagnostics,
|
|
2707
|
+
source,
|
|
2708
|
+
token.start,
|
|
2709
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
2710
|
+
);
|
|
2711
|
+
const role = attr(token, "role");
|
|
2712
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
2713
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
2714
|
+
const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
|
|
2715
|
+
const supportedStyles = definition?.styles;
|
|
2716
|
+
const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
|
|
2717
|
+
if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
|
|
2718
|
+
addDiagnostic(
|
|
2719
|
+
diagnostics,
|
|
2720
|
+
source,
|
|
2721
|
+
token.start,
|
|
2722
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
2723
|
+
severity,
|
|
2724
|
+
"azure-unsupported-style"
|
|
2725
|
+
);
|
|
2726
|
+
if (style && voiceName && !definition && severity)
|
|
2727
|
+
addDiagnostic(
|
|
2728
|
+
diagnostics,
|
|
2729
|
+
source,
|
|
2730
|
+
token.start,
|
|
2731
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
2732
|
+
severity
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
if (name === "say-as" || name === "sayas") {
|
|
2736
|
+
const interpretAs = attr(token, "interpret-as");
|
|
2737
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
2738
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
2739
|
+
}
|
|
2740
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
2741
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
2742
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
2743
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
2744
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
2745
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
2746
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
2747
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
2748
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
2749
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
2750
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
2751
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
2752
|
+
if (name === "lexicon") {
|
|
2753
|
+
const uri = attr(token, "uri");
|
|
2754
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
2755
|
+
else {
|
|
2756
|
+
try {
|
|
2757
|
+
const parsed = new URL(uri);
|
|
2758
|
+
if (parsed.protocol !== "https:")
|
|
2759
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
2760
|
+
} catch {
|
|
2761
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (name === "mstts:silence") {
|
|
2766
|
+
const type = attr(token, "type");
|
|
2767
|
+
const value = attr(token, "value");
|
|
2768
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
2769
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
2770
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
2771
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
2772
|
+
}
|
|
2773
|
+
if (name === "mstts:audioduration") {
|
|
2774
|
+
const value = attr(token, "value");
|
|
2775
|
+
if (!value || !isValidAzureAudioDuration(value))
|
|
2776
|
+
addDiagnostic(
|
|
2777
|
+
diagnostics,
|
|
2778
|
+
source,
|
|
2779
|
+
token.start,
|
|
2780
|
+
'<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
|
|
2781
|
+
);
|
|
2782
|
+
if (!token.selfClosing)
|
|
2783
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
|
|
2784
|
+
}
|
|
2785
|
+
if (name === "mstts:viseme") {
|
|
2786
|
+
const type = attr(token, "type");
|
|
2787
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
2788
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
2789
|
+
}
|
|
2790
|
+
if (name === "audio") {
|
|
2791
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
2792
|
+
}
|
|
2793
|
+
if (name === "mstts:turn") {
|
|
2794
|
+
if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
|
|
2795
|
+
addDiagnostic(
|
|
2796
|
+
diagnostics,
|
|
2797
|
+
source,
|
|
2798
|
+
token.start,
|
|
2799
|
+
'<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
|
|
2800
|
+
);
|
|
2801
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
2802
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
2803
|
+
}
|
|
2804
|
+
if (name === "mstts:backgroundaudio") {
|
|
2805
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
2806
|
+
const volume = attr(token, "volume");
|
|
2807
|
+
if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
|
|
2808
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
2809
|
+
for (const [attribute, value] of [
|
|
2810
|
+
["fadein", attr(token, "fadein")],
|
|
2811
|
+
["fadeout", attr(token, "fadeout")]
|
|
2812
|
+
]) {
|
|
2813
|
+
if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
|
|
2814
|
+
addDiagnostic(
|
|
2815
|
+
diagnostics,
|
|
2816
|
+
source,
|
|
2817
|
+
token.start,
|
|
2818
|
+
`<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
|
|
2822
|
+
addDiagnostic(
|
|
2823
|
+
diagnostics,
|
|
2824
|
+
source,
|
|
2825
|
+
token.start,
|
|
2826
|
+
"<mstts:backgroundaudio> must be the first element directly under <speak>."
|
|
2827
|
+
);
|
|
2828
|
+
if (!token.selfClosing)
|
|
2829
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
2833
|
+
const diagnostics = [];
|
|
2834
|
+
if (typeof ssml !== "string") {
|
|
2835
|
+
return [
|
|
2836
|
+
{
|
|
2837
|
+
line: 1,
|
|
2838
|
+
column: 1,
|
|
2839
|
+
message: "SSML input must be a string",
|
|
2840
|
+
severity: "error",
|
|
2841
|
+
source: "ssml-static-validator"
|
|
2842
|
+
}
|
|
2843
|
+
];
|
|
2844
|
+
}
|
|
2845
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
2846
|
+
if (ssml.length > maxLength)
|
|
2847
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
2848
|
+
try {
|
|
2849
|
+
parseSsml(ssml);
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
2852
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
2853
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
2854
|
+
return diagnostics;
|
|
2855
|
+
}
|
|
2856
|
+
const tokens = tokenizeElements(ssml);
|
|
2857
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
2858
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
2859
|
+
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
2860
|
+
for (const [index, token] of backgroundAudioTokens.entries()) {
|
|
2861
|
+
if (index > 0)
|
|
2862
|
+
addDiagnostic(
|
|
2863
|
+
diagnostics,
|
|
2864
|
+
ssml,
|
|
2865
|
+
token.start,
|
|
2866
|
+
"An SSML document can contain at most one <mstts:backgroundaudio> element."
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
if (!speak || voices.length === 0)
|
|
2870
|
+
addDiagnostic(
|
|
2871
|
+
diagnostics,
|
|
2872
|
+
ssml,
|
|
2873
|
+
speak?.start ?? 0,
|
|
2874
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
2875
|
+
);
|
|
2876
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
2877
|
+
const voiceCatalog = normalizeVoiceCatalog(options);
|
|
2878
|
+
const normalizeLanguage = createLanguageNormalizer(options);
|
|
2879
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
2880
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
2881
|
+
for (const token of voicesToValidate) {
|
|
2882
|
+
const name = attr(token, "name")?.trim();
|
|
2883
|
+
const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
|
|
2884
|
+
const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
|
|
2885
|
+
if (name && !definition && policySeverity)
|
|
2886
|
+
addDiagnostic(
|
|
2887
|
+
diagnostics,
|
|
2888
|
+
ssml,
|
|
2889
|
+
token.start,
|
|
2890
|
+
`Unknown voice "${name}" is not registered in the voice catalog.`,
|
|
2891
|
+
policySeverity,
|
|
2892
|
+
"azure-unknown-voice"
|
|
2893
|
+
);
|
|
2894
|
+
if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
|
|
2895
|
+
addDiagnostic(
|
|
2896
|
+
diagnostics,
|
|
2897
|
+
ssml,
|
|
2898
|
+
token.start,
|
|
2899
|
+
`Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
|
|
2900
|
+
"warning",
|
|
2901
|
+
"azure-locale-mismatch"
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
for (const token of tokens) {
|
|
2905
|
+
const tokenName = token.name.toLowerCase();
|
|
2906
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
2907
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2908
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2909
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2910
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2911
|
+
addDiagnostic(
|
|
2912
|
+
diagnostics,
|
|
2913
|
+
ssml,
|
|
2914
|
+
token.start,
|
|
2915
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
2916
|
+
"error",
|
|
2917
|
+
"azure-unsupported-model-for-voice"
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
return diagnostics;
|
|
2922
|
+
}
|
|
2923
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
2924
|
+
apiVersion: "2025-10-01",
|
|
2925
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
2926
|
+
regions: [],
|
|
2927
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
2928
|
+
};
|
|
2160
2929
|
|
|
2161
2930
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
2162
2931
|
function getDocumentChildren(document) {
|
|
@@ -2281,7 +3050,7 @@ function validateEntityReferences(value) {
|
|
|
2281
3050
|
index = semicolon + 1;
|
|
2282
3051
|
}
|
|
2283
3052
|
}
|
|
2284
|
-
function
|
|
3053
|
+
function findTagEnd3(source, start, hasInternalSubset = false) {
|
|
2285
3054
|
let quote;
|
|
2286
3055
|
let subsetDepth = 0;
|
|
2287
3056
|
for (let index = start + 1; index < source.length; index += 1) {
|
|
@@ -2454,7 +3223,7 @@ function parseXml(source) {
|
|
|
2454
3223
|
continue;
|
|
2455
3224
|
}
|
|
2456
3225
|
if (source.startsWith("</", index)) {
|
|
2457
|
-
const end2 =
|
|
3226
|
+
const end2 = findTagEnd3(source, index);
|
|
2458
3227
|
const name2 = parseClosingTag(source, index, end2);
|
|
2459
3228
|
const element = currentElement(stack);
|
|
2460
3229
|
if (element === void 0 || element.name !== name2) {
|
|
@@ -2466,7 +3235,7 @@ function parseXml(source) {
|
|
|
2466
3235
|
continue;
|
|
2467
3236
|
}
|
|
2468
3237
|
if (source.startsWith("<!", index)) {
|
|
2469
|
-
const end2 =
|
|
3238
|
+
const end2 = findTagEnd3(source, index, true);
|
|
2470
3239
|
parseDeclaration(source, index, end2);
|
|
2471
3240
|
appendNode2(currentElement(stack)?.children ?? children, {
|
|
2472
3241
|
kind: "declaration",
|
|
@@ -2475,7 +3244,7 @@ function parseXml(source) {
|
|
|
2475
3244
|
index = end2 + 1;
|
|
2476
3245
|
continue;
|
|
2477
3246
|
}
|
|
2478
|
-
const end =
|
|
3247
|
+
const end = findTagEnd3(source, index);
|
|
2479
3248
|
const { name, selfClosing } = parseStartTag(source, index, end);
|
|
2480
3249
|
if (stack.length === 0) {
|
|
2481
3250
|
if (root !== void 0) {
|
|
@@ -2942,6 +3711,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
2942
3711
|
label: "mstts:audioduration",
|
|
2943
3712
|
insertText: '<mstts:audioduration value="10s" />'
|
|
2944
3713
|
},
|
|
3714
|
+
{
|
|
3715
|
+
label: "mstts:dialog",
|
|
3716
|
+
insertText: `<mstts:dialog>
|
|
3717
|
+
<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
|
|
3718
|
+
</mstts:dialog>`
|
|
3719
|
+
},
|
|
3720
|
+
{
|
|
3721
|
+
label: "mstts:turn",
|
|
3722
|
+
insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
|
|
3723
|
+
},
|
|
3724
|
+
{
|
|
3725
|
+
label: "mstts:backgroundaudio",
|
|
3726
|
+
insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
label: "mstts:ttsembedding",
|
|
3730
|
+
insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
|
|
3731
|
+
},
|
|
2945
3732
|
{
|
|
2946
3733
|
label: "sub",
|
|
2947
3734
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -3312,6 +4099,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
3312
4099
|
example: "10s"
|
|
3313
4100
|
}
|
|
3314
4101
|
]
|
|
4102
|
+
},
|
|
4103
|
+
{
|
|
4104
|
+
name: "mstts:dialog",
|
|
4105
|
+
description: "Groups multiple Azure dialog turns that can use different voices.",
|
|
4106
|
+
parameters: []
|
|
4107
|
+
},
|
|
4108
|
+
{
|
|
4109
|
+
name: "mstts:turn",
|
|
4110
|
+
description: "Adds one dialog turn using the required Azure voice name.",
|
|
4111
|
+
parameters: [
|
|
4112
|
+
{
|
|
4113
|
+
name: "voice",
|
|
4114
|
+
description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
|
|
4115
|
+
example: "en-US-JennyNeural"
|
|
4116
|
+
}
|
|
4117
|
+
]
|
|
4118
|
+
},
|
|
4119
|
+
{
|
|
4120
|
+
name: "mstts:backgroundaudio",
|
|
4121
|
+
description: "Plays background audio while speech is synthesized.",
|
|
4122
|
+
parameters: [
|
|
4123
|
+
{
|
|
4124
|
+
name: "src",
|
|
4125
|
+
description: "An absolute HTTP(S) URL for the background audio file.",
|
|
4126
|
+
example: "https://example.com/music.mp3"
|
|
4127
|
+
},
|
|
4128
|
+
{
|
|
4129
|
+
name: "volume",
|
|
4130
|
+
description: "The background audio volume, for example `-3dB` or `medium`.",
|
|
4131
|
+
example: "-3dB"
|
|
4132
|
+
},
|
|
4133
|
+
{
|
|
4134
|
+
name: "fadein",
|
|
4135
|
+
description: "The fade-in duration, for example `1s`.",
|
|
4136
|
+
example: "1s"
|
|
4137
|
+
},
|
|
4138
|
+
{
|
|
4139
|
+
name: "fadeout",
|
|
4140
|
+
description: "The fade-out duration, for example `500ms`.",
|
|
4141
|
+
example: "500ms"
|
|
4142
|
+
}
|
|
4143
|
+
]
|
|
4144
|
+
},
|
|
4145
|
+
{
|
|
4146
|
+
name: "mstts:ttsembedding",
|
|
4147
|
+
description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
|
|
4148
|
+
parameters: []
|
|
4149
|
+
},
|
|
4150
|
+
{
|
|
4151
|
+
name: "mstts:embedding",
|
|
4152
|
+
description: "Specifies embedding metadata for custom voice scenarios.",
|
|
4153
|
+
parameters: []
|
|
4154
|
+
},
|
|
4155
|
+
{
|
|
4156
|
+
name: "mstts:voiceconversion",
|
|
4157
|
+
description: "Specifies voice conversion metadata for custom voice scenarios.",
|
|
4158
|
+
parameters: []
|
|
3315
4159
|
}
|
|
3316
4160
|
];
|
|
3317
4161
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -3370,7 +4214,7 @@ function toRange(source, token) {
|
|
|
3370
4214
|
function containsOffset(token, offset) {
|
|
3371
4215
|
return offset >= token.start && offset < token.end;
|
|
3372
4216
|
}
|
|
3373
|
-
function
|
|
4217
|
+
function findTagEnd4(source, start) {
|
|
3374
4218
|
let quote;
|
|
3375
4219
|
for (let index = start; index < source.length; index += 1) {
|
|
3376
4220
|
const character = source[index];
|
|
@@ -3501,7 +4345,7 @@ function findTagAtOffset(source, offset) {
|
|
|
3501
4345
|
searchStart = tokenEnd2;
|
|
3502
4346
|
continue;
|
|
3503
4347
|
}
|
|
3504
|
-
const tagEnd =
|
|
4348
|
+
const tagEnd = findTagEnd4(source, start + 1);
|
|
3505
4349
|
const contentEnd = tagEnd ?? source.length;
|
|
3506
4350
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
3507
4351
|
if (offset < tokenEnd) {
|
|
@@ -3650,6 +4494,7 @@ export {
|
|
|
3650
4494
|
parseSsml,
|
|
3651
4495
|
buildPartialSsml,
|
|
3652
4496
|
validateSsml,
|
|
4497
|
+
validateAzureSsml,
|
|
3653
4498
|
clearSsmlDocument,
|
|
3654
4499
|
formatXmlFragment,
|
|
3655
4500
|
getEditableRegion,
|
|
@@ -3663,4 +4508,4 @@ export {
|
|
|
3663
4508
|
findSsmlHoverTarget,
|
|
3664
4509
|
formatSsmlHover
|
|
3665
4510
|
};
|
|
3666
|
-
//# sourceMappingURL=chunk-
|
|
4511
|
+
//# sourceMappingURL=chunk-BYIZQL2W.mjs.map
|