ssml-builder-js 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -8
- package/dist/{chunk-VCDMKVVT.mjs → chunk-7LCSH4SZ.mjs} +457 -25
- package/dist/chunk-7LCSH4SZ.mjs.map +1 -0
- package/dist/{chunk-MWSDFGXW.mjs → chunk-JNJVTEL6.mjs} +785 -9
- package/dist/chunk-JNJVTEL6.mjs.map +1 -0
- package/dist/core.d.mts +91 -3
- package/dist/core.d.ts +91 -3
- package/dist/core.js +462 -25
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +13 -3
- package/dist/elements.d.mts +10 -1
- package/dist/elements.d.ts +10 -1
- package/dist/elements.js +1071 -13
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +292 -8
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +462 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +40 -3
- package/dist/react.d.ts +40 -3
- package/dist/react.js +1026 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +237 -5
- package/dist/react.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-MWSDFGXW.mjs.map +0 -1
- package/dist/chunk-VCDMKVVT.mjs.map +0 -1
|
@@ -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,7 @@ var SSML_ATTRS = {
|
|
|
1352
1363
|
LANG: "lang",
|
|
1353
1364
|
MSTTS_XMLNS: "xmlns:mstts",
|
|
1354
1365
|
NAME: "name",
|
|
1366
|
+
VOICE: "voice",
|
|
1355
1367
|
EFFECT: "effect",
|
|
1356
1368
|
RATE: "rate",
|
|
1357
1369
|
PITCH: "pitch",
|
|
@@ -1383,7 +1395,9 @@ var SSML_ATTRS = {
|
|
|
1383
1395
|
MARK: "mark",
|
|
1384
1396
|
URI: "uri",
|
|
1385
1397
|
TYPE: "type",
|
|
1386
|
-
VALUE: "value"
|
|
1398
|
+
VALUE: "value",
|
|
1399
|
+
FADE_IN: "fadein",
|
|
1400
|
+
FADE_OUT: "fadeout"
|
|
1387
1401
|
};
|
|
1388
1402
|
function escapeText(value) {
|
|
1389
1403
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1473,6 +1487,20 @@ function getAttributes(element) {
|
|
|
1473
1487
|
case SSML_TAGS.MSTTS_AUDIO_DURATION:
|
|
1474
1488
|
addAttribute(attributes, SSML_ATTRS.VALUE, element.value);
|
|
1475
1489
|
break;
|
|
1490
|
+
case SSML_TAGS.MSTTS_TURN:
|
|
1491
|
+
addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
|
|
1492
|
+
break;
|
|
1493
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
|
|
1494
|
+
addAttribute(attributes, SSML_ATTRS.SRC, element.src);
|
|
1495
|
+
addAttribute(attributes, SSML_ATTRS.VOLUME, element.volume);
|
|
1496
|
+
addAttribute(attributes, SSML_ATTRS.FADE_IN, element.fadeIn ?? element.fadein);
|
|
1497
|
+
addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
|
|
1498
|
+
break;
|
|
1499
|
+
case SSML_TAGS.MSTTS_DIALOG:
|
|
1500
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING:
|
|
1501
|
+
case SSML_TAGS.MSTTS_EMBEDDING:
|
|
1502
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION:
|
|
1503
|
+
break;
|
|
1476
1504
|
case SSML_TAGS.PARAGRAPH:
|
|
1477
1505
|
case SSML_TAGS.SENTENCE:
|
|
1478
1506
|
case SSML_TAGS.WORD:
|
|
@@ -2037,6 +2065,40 @@ function convertElement(node) {
|
|
|
2037
2065
|
if (value !== void 0) element.value = value;
|
|
2038
2066
|
return finishElement(element, node, attributes);
|
|
2039
2067
|
}
|
|
2068
|
+
case SSML_TAGS.MSTTS_DIALOG: {
|
|
2069
|
+
const element = { type: SSML_TAGS.MSTTS_DIALOG };
|
|
2070
|
+
return finishElement(element, node, attributes);
|
|
2071
|
+
}
|
|
2072
|
+
case SSML_TAGS.MSTTS_TURN: {
|
|
2073
|
+
const element = { type: SSML_TAGS.MSTTS_TURN };
|
|
2074
|
+
const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
|
|
2075
|
+
if (voice !== void 0) element.voice = voice;
|
|
2076
|
+
return finishElement(element, node, attributes);
|
|
2077
|
+
}
|
|
2078
|
+
case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
|
|
2079
|
+
const element = { type: SSML_TAGS.MSTTS_BACKGROUND_AUDIO };
|
|
2080
|
+
const src = readAttribute(attributes, SSML_ATTRS.SRC);
|
|
2081
|
+
const volume = readAttribute(attributes, SSML_ATTRS.VOLUME);
|
|
2082
|
+
const fadeIn = readAttribute(attributes, SSML_ATTRS.FADE_IN);
|
|
2083
|
+
const fadeOut = readAttribute(attributes, SSML_ATTRS.FADE_OUT);
|
|
2084
|
+
if (src !== void 0) element.src = src;
|
|
2085
|
+
if (volume !== void 0) element.volume = volume;
|
|
2086
|
+
if (fadeIn !== void 0) element.fadeIn = fadeIn;
|
|
2087
|
+
if (fadeOut !== void 0) element.fadeOut = fadeOut;
|
|
2088
|
+
return finishElement(element, node, attributes);
|
|
2089
|
+
}
|
|
2090
|
+
case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
|
|
2091
|
+
const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
|
|
2092
|
+
return finishElement(element, node, attributes);
|
|
2093
|
+
}
|
|
2094
|
+
case SSML_TAGS.MSTTS_EMBEDDING: {
|
|
2095
|
+
const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
|
|
2096
|
+
return finishElement(element, node, attributes);
|
|
2097
|
+
}
|
|
2098
|
+
case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
|
|
2099
|
+
const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
|
|
2100
|
+
return finishElement(element, node, attributes);
|
|
2101
|
+
}
|
|
2040
2102
|
default: {
|
|
2041
2103
|
const element = {
|
|
2042
2104
|
name: node.name,
|
|
@@ -2157,6 +2219,644 @@ function validateSsml(xmlString) {
|
|
|
2157
2219
|
};
|
|
2158
2220
|
}
|
|
2159
2221
|
}
|
|
2222
|
+
var AZURE_VOICE_DEFINITIONS = [
|
|
2223
|
+
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2224
|
+
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
2225
|
+
{ name: "en-US-AndrewNeural", locale: "en-US", styles: ["empathetic", "relieved"] },
|
|
2226
|
+
{
|
|
2227
|
+
name: "en-US-GuyNeural",
|
|
2228
|
+
locale: "en-US",
|
|
2229
|
+
styles: [
|
|
2230
|
+
"angry",
|
|
2231
|
+
"cheerful",
|
|
2232
|
+
"excited",
|
|
2233
|
+
"friendly",
|
|
2234
|
+
"hopeful",
|
|
2235
|
+
"newscast",
|
|
2236
|
+
"sad",
|
|
2237
|
+
"shouting",
|
|
2238
|
+
"terrified",
|
|
2239
|
+
"unfriendly",
|
|
2240
|
+
"whispering"
|
|
2241
|
+
]
|
|
2242
|
+
},
|
|
2243
|
+
{
|
|
2244
|
+
name: "en-US-JennyMultilingualNeural",
|
|
2245
|
+
locale: "en-US",
|
|
2246
|
+
styles: [
|
|
2247
|
+
"cheerful",
|
|
2248
|
+
"empathetic",
|
|
2249
|
+
"excited",
|
|
2250
|
+
"friendly",
|
|
2251
|
+
"hopeful",
|
|
2252
|
+
"sad",
|
|
2253
|
+
"shouting",
|
|
2254
|
+
"terrified",
|
|
2255
|
+
"unfriendly",
|
|
2256
|
+
"whispering"
|
|
2257
|
+
]
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
name: "en-US-JennyNeural",
|
|
2261
|
+
locale: "en-US",
|
|
2262
|
+
styles: [
|
|
2263
|
+
"assistant",
|
|
2264
|
+
"chat",
|
|
2265
|
+
"customerservice",
|
|
2266
|
+
"newscast",
|
|
2267
|
+
"cheerful",
|
|
2268
|
+
"empathetic",
|
|
2269
|
+
"excited",
|
|
2270
|
+
"friendly",
|
|
2271
|
+
"hopeful",
|
|
2272
|
+
"sad",
|
|
2273
|
+
"shouting",
|
|
2274
|
+
"terrified",
|
|
2275
|
+
"unfriendly",
|
|
2276
|
+
"whispering"
|
|
2277
|
+
]
|
|
2278
|
+
},
|
|
2279
|
+
{ name: "es-ES-ElviraNeural", locale: "es-ES" },
|
|
2280
|
+
{ name: "fil-PH-AngeloNeural", locale: "fil-PH" },
|
|
2281
|
+
{ name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2282
|
+
{ name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
|
|
2283
|
+
{ name: "id-ID-GadisNeural", locale: "id-ID" },
|
|
2284
|
+
{ name: "it-IT-ElsaNeural", locale: "it-IT", styles: ["cheerful", "sad"] },
|
|
2285
|
+
{ name: "ja-JP-KeitaNeural", locale: "ja-JP", styles: ["chat"] },
|
|
2286
|
+
{ name: "ja-JP-MayuNeural", locale: "ja-JP", styles: ["calm", "cheerful", "sad"] },
|
|
2287
|
+
{ name: "ja-JP-NanamiNeural", locale: "ja-JP", styles: ["chat", "customerservice", "cheerful", "whispering", "sad"] },
|
|
2288
|
+
{ name: "ko-KR-SunHiNeural", locale: "ko-KR", styles: ["cheerful", "sad"] },
|
|
2289
|
+
{ name: "ms-MY-YasminNeural", locale: "ms-MY" },
|
|
2290
|
+
{ name: "pt-BR-FranciscaNeural", locale: "pt-BR", styles: ["calm"] },
|
|
2291
|
+
{
|
|
2292
|
+
name: "ru-RU-SvetlanaNeural",
|
|
2293
|
+
locale: "ru-RU",
|
|
2294
|
+
styles: ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
2295
|
+
},
|
|
2296
|
+
{ name: "th-TH-PremwadeeNeural", locale: "th-TH" },
|
|
2297
|
+
{ name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
|
|
2298
|
+
{
|
|
2299
|
+
name: "zh-CN-XiaoxiaoNeural",
|
|
2300
|
+
locale: "zh-CN",
|
|
2301
|
+
styles: [
|
|
2302
|
+
"assistant",
|
|
2303
|
+
"chat",
|
|
2304
|
+
"customerservice",
|
|
2305
|
+
"newscast",
|
|
2306
|
+
"cheerful",
|
|
2307
|
+
"empathetic",
|
|
2308
|
+
"excited",
|
|
2309
|
+
"friendly",
|
|
2310
|
+
"hopeful",
|
|
2311
|
+
"sad",
|
|
2312
|
+
"terrified",
|
|
2313
|
+
"whispering",
|
|
2314
|
+
"poetry-reading",
|
|
2315
|
+
"sports_commentary",
|
|
2316
|
+
"sports_commentary_excited",
|
|
2317
|
+
"story"
|
|
2318
|
+
]
|
|
2319
|
+
},
|
|
2320
|
+
{
|
|
2321
|
+
name: "zh-CN-YunxiNeural",
|
|
2322
|
+
locale: "zh-CN",
|
|
2323
|
+
styles: [
|
|
2324
|
+
"narration-relaxed",
|
|
2325
|
+
"embarrassed",
|
|
2326
|
+
"fearful",
|
|
2327
|
+
"sad",
|
|
2328
|
+
"disgruntled",
|
|
2329
|
+
"serious",
|
|
2330
|
+
"angry",
|
|
2331
|
+
"depressed",
|
|
2332
|
+
"chat",
|
|
2333
|
+
"cheerful",
|
|
2334
|
+
"assistant"
|
|
2335
|
+
]
|
|
2336
|
+
},
|
|
2337
|
+
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
2338
|
+
];
|
|
2339
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
2340
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
2341
|
+
"characters",
|
|
2342
|
+
"spell-out",
|
|
2343
|
+
"cardinal",
|
|
2344
|
+
"ordinal",
|
|
2345
|
+
"number",
|
|
2346
|
+
"date",
|
|
2347
|
+
"time",
|
|
2348
|
+
"telephone",
|
|
2349
|
+
"fraction",
|
|
2350
|
+
"address",
|
|
2351
|
+
"name",
|
|
2352
|
+
"currency",
|
|
2353
|
+
"number_digit"
|
|
2354
|
+
]);
|
|
2355
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
2356
|
+
"Girl",
|
|
2357
|
+
"Boy",
|
|
2358
|
+
"YoungAdultFemale",
|
|
2359
|
+
"YoungAdultMale",
|
|
2360
|
+
"OlderAdultFemale",
|
|
2361
|
+
"OlderAdultMale",
|
|
2362
|
+
"SeniorFemale",
|
|
2363
|
+
"SeniorMale"
|
|
2364
|
+
]);
|
|
2365
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
2366
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
2367
|
+
"Leading",
|
|
2368
|
+
"Tailing",
|
|
2369
|
+
"Sentenceboundary",
|
|
2370
|
+
"Comma",
|
|
2371
|
+
"Semicolon",
|
|
2372
|
+
"Enumerationcomma"
|
|
2373
|
+
]);
|
|
2374
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
2375
|
+
function decodeAttribute(value) {
|
|
2376
|
+
return value.replace(
|
|
2377
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
2378
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
function findTagEnd2(source, start) {
|
|
2382
|
+
let quote = "";
|
|
2383
|
+
for (let index = start; index < source.length; index += 1) {
|
|
2384
|
+
const character = source[index];
|
|
2385
|
+
if (quote) {
|
|
2386
|
+
if (character === quote) quote = "";
|
|
2387
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
2388
|
+
else if (character === ">") return index;
|
|
2389
|
+
}
|
|
2390
|
+
return source.length - 1;
|
|
2391
|
+
}
|
|
2392
|
+
function tokenizeElements(source) {
|
|
2393
|
+
const tokens = [];
|
|
2394
|
+
const openElements = [];
|
|
2395
|
+
let index = 0;
|
|
2396
|
+
while (index < source.length) {
|
|
2397
|
+
const start = source.indexOf("<", index);
|
|
2398
|
+
if (start === -1) break;
|
|
2399
|
+
if (source.startsWith("<!--", start)) {
|
|
2400
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
2401
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
2405
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
2406
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (source.startsWith("<?", start)) {
|
|
2410
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
2411
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
const end = findTagEnd2(source, start + 1);
|
|
2415
|
+
const raw = source.slice(start, end + 1);
|
|
2416
|
+
if (raw.startsWith("</")) {
|
|
2417
|
+
openElements.pop();
|
|
2418
|
+
index = end + 1;
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
2422
|
+
if (!nameMatch?.[1]) {
|
|
2423
|
+
index = end + 1;
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
2427
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
2428
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
2429
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
2430
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
2431
|
+
}
|
|
2432
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
2433
|
+
const parent = openElements[openElements.length - 1];
|
|
2434
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
2435
|
+
const tokenName = nameMatch[1];
|
|
2436
|
+
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
2437
|
+
tokens.push({
|
|
2438
|
+
attributes,
|
|
2439
|
+
end,
|
|
2440
|
+
name: tokenName,
|
|
2441
|
+
parentName: parent?.name,
|
|
2442
|
+
parentVoiceName,
|
|
2443
|
+
selfClosing,
|
|
2444
|
+
start
|
|
2445
|
+
});
|
|
2446
|
+
if (!selfClosing) {
|
|
2447
|
+
openElements.push({
|
|
2448
|
+
name: tokenName,
|
|
2449
|
+
voiceName: tokenVoiceName
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
index = end + 1;
|
|
2453
|
+
}
|
|
2454
|
+
return tokens;
|
|
2455
|
+
}
|
|
2456
|
+
function location(source, offset) {
|
|
2457
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
2458
|
+
const line = before.split("\n").length;
|
|
2459
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
2460
|
+
}
|
|
2461
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
|
|
2462
|
+
diagnostics.push({
|
|
2463
|
+
...location(source, offset),
|
|
2464
|
+
message,
|
|
2465
|
+
severity,
|
|
2466
|
+
source: "ssml-static-validator",
|
|
2467
|
+
...code2 ? { code: code2 } : {}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
function isSupportedProsodyRate(value) {
|
|
2471
|
+
const trimmed = value.trim();
|
|
2472
|
+
if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
|
|
2473
|
+
const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
|
|
2474
|
+
if (!multiplier) return false;
|
|
2475
|
+
const numericValue = Number(multiplier[1]);
|
|
2476
|
+
return numericValue >= 0.5 && numericValue <= 2;
|
|
2477
|
+
}
|
|
2478
|
+
function isValidAzureAudioDuration(value) {
|
|
2479
|
+
const trimmed = value.trim();
|
|
2480
|
+
const numeric = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(trimmed);
|
|
2481
|
+
if (numeric) return Number(numeric[1]) > 0;
|
|
2482
|
+
const clock = /^(\d{2,}):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed);
|
|
2483
|
+
if (!clock) return false;
|
|
2484
|
+
return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
|
|
2485
|
+
}
|
|
2486
|
+
function attr(token, name) {
|
|
2487
|
+
return token.attributes.get(name.toLowerCase());
|
|
2488
|
+
}
|
|
2489
|
+
var DEFAULT_LANGUAGE_ALIASES = {
|
|
2490
|
+
"zh-CN": ["zh-Hans"],
|
|
2491
|
+
"zh-TW": ["zh-Hant"]
|
|
2492
|
+
};
|
|
2493
|
+
function canonicalLanguageTag(language) {
|
|
2494
|
+
const trimmed = language.trim();
|
|
2495
|
+
if (!trimmed) return "";
|
|
2496
|
+
try {
|
|
2497
|
+
return new Intl.Locale(trimmed).toString().toLowerCase();
|
|
2498
|
+
} catch {
|
|
2499
|
+
return trimmed.toLowerCase();
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
function createLanguageNormalizer(options) {
|
|
2503
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2504
|
+
const addAliasGroup = (canonical, values) => {
|
|
2505
|
+
const normalizedCanonical = canonicalLanguageTag(canonical);
|
|
2506
|
+
if (!normalizedCanonical) return;
|
|
2507
|
+
aliases.set(normalizedCanonical, normalizedCanonical);
|
|
2508
|
+
for (const value of values) {
|
|
2509
|
+
const normalizedValue = canonicalLanguageTag(value);
|
|
2510
|
+
if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
|
|
2514
|
+
for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
|
|
2515
|
+
addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
|
|
2516
|
+
return (language) => {
|
|
2517
|
+
const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
|
|
2518
|
+
const normalized = canonicalLanguageTag(customValue);
|
|
2519
|
+
return aliases.get(normalized) ?? normalized;
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function voiceLocalePrefix(voiceName) {
|
|
2523
|
+
const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
|
|
2524
|
+
if (!match?.groups) return void 0;
|
|
2525
|
+
const tag = `${match.groups.language}-${match.groups.region}`;
|
|
2526
|
+
return {
|
|
2527
|
+
language: match.groups.language.toLowerCase(),
|
|
2528
|
+
region: match.groups.region.toLowerCase(),
|
|
2529
|
+
tag
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function definitionFromStyleMap(voiceName, styles) {
|
|
2533
|
+
return {
|
|
2534
|
+
name: voiceName,
|
|
2535
|
+
locale: voiceLocalePrefix(voiceName)?.tag ?? "",
|
|
2536
|
+
styles
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
function normalizeVoiceCatalog(options) {
|
|
2540
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2541
|
+
for (const definition of AZURE_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
|
|
2542
|
+
for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2543
|
+
for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
|
|
2544
|
+
for (const definition of options.customVoiceDefinitions ?? [])
|
|
2545
|
+
definitions.set(definition.name.toLowerCase(), definition);
|
|
2546
|
+
for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
|
|
2547
|
+
const key = voiceName.toLowerCase();
|
|
2548
|
+
const current = definitions.get(key);
|
|
2549
|
+
definitions.set(key, {
|
|
2550
|
+
...current ?? definitionFromStyleMap(voiceName, styles),
|
|
2551
|
+
name: current?.name ?? voiceName,
|
|
2552
|
+
styles: styles.map((style) => style.toLowerCase())
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
return definitions;
|
|
2556
|
+
}
|
|
2557
|
+
function diagnosticSeverity(policy) {
|
|
2558
|
+
if (policy === "ignore") return void 0;
|
|
2559
|
+
return policy === "error" ? "error" : "warning";
|
|
2560
|
+
}
|
|
2561
|
+
function languagePart(language) {
|
|
2562
|
+
try {
|
|
2563
|
+
return new Intl.Locale(language).language.toLowerCase();
|
|
2564
|
+
} catch {
|
|
2565
|
+
return language.split("-")[0]?.toLowerCase() ?? "";
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
|
|
2569
|
+
const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
|
|
2570
|
+
if (candidateLanguages.length === 0 || !language.trim()) return void 0;
|
|
2571
|
+
const normalizedLanguage = normalizeLanguage(language);
|
|
2572
|
+
const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
|
|
2573
|
+
if (normalizedCandidates.includes(normalizedLanguage)) return true;
|
|
2574
|
+
if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
|
|
2575
|
+
return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
|
|
2576
|
+
}
|
|
2577
|
+
function canonicalTagName(name) {
|
|
2578
|
+
const normalized = name.toLowerCase();
|
|
2579
|
+
if (normalized === "express-as" || normalized === "expressas") return "mstts:express-as";
|
|
2580
|
+
if (normalized === "sayas") return "say-as";
|
|
2581
|
+
return normalized;
|
|
2582
|
+
}
|
|
2583
|
+
function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, definition) {
|
|
2584
|
+
if (!voiceName || !definition || token.name.toLowerCase() === "voice" || token.name.toLowerCase() === "mstts:turn")
|
|
2585
|
+
return;
|
|
2586
|
+
const tagName = canonicalTagName(token.name);
|
|
2587
|
+
const unsupportedTags = new Set((definition.unsupportedTags ?? []).map(canonicalTagName));
|
|
2588
|
+
const supportedTags = definition.supportedTags?.map(canonicalTagName);
|
|
2589
|
+
if (unsupportedTags.has(tagName) || supportedTags !== void 0 && !supportedTags.includes(tagName)) {
|
|
2590
|
+
addDiagnostic(
|
|
2591
|
+
diagnostics,
|
|
2592
|
+
source,
|
|
2593
|
+
token.start,
|
|
2594
|
+
`Tag <${token.name}> is not supported by voice "${voiceName}" according to the configured feature matrix.`,
|
|
2595
|
+
"error",
|
|
2596
|
+
"azure-unsupported-tag-for-voice"
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
function validateAudioSource(token, source, diagnostics, options, elementName2) {
|
|
2601
|
+
const src = attr(token, "src");
|
|
2602
|
+
if (!src) {
|
|
2603
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
let parsed;
|
|
2607
|
+
try {
|
|
2608
|
+
parsed = new URL(src);
|
|
2609
|
+
} catch {
|
|
2610
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2614
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
2615
|
+
if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
|
|
2616
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
|
|
2617
|
+
else if (!options.allowExternalAudio)
|
|
2618
|
+
addDiagnostic(
|
|
2619
|
+
diagnostics,
|
|
2620
|
+
source,
|
|
2621
|
+
token.start,
|
|
2622
|
+
`<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
2626
|
+
const name = token.name.toLowerCase();
|
|
2627
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
2628
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
2629
|
+
if (name === "break") {
|
|
2630
|
+
const time = attr(token, "time");
|
|
2631
|
+
const strength = attr(token, "strength");
|
|
2632
|
+
if (!time && !strength)
|
|
2633
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
2634
|
+
if (time && strength)
|
|
2635
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
2636
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
2637
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
2638
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
2639
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
2640
|
+
}
|
|
2641
|
+
if (name === "prosody") {
|
|
2642
|
+
const rate = attr(token, "rate");
|
|
2643
|
+
const pitch = attr(token, "pitch");
|
|
2644
|
+
const volume = attr(token, "volume");
|
|
2645
|
+
if (rate && !isSupportedProsodyRate(rate))
|
|
2646
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
2647
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
2648
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
2649
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
2650
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
2651
|
+
}
|
|
2652
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
2653
|
+
const style = attr(token, "style");
|
|
2654
|
+
if (!style?.trim())
|
|
2655
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
2656
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
2657
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
2658
|
+
addDiagnostic(
|
|
2659
|
+
diagnostics,
|
|
2660
|
+
source,
|
|
2661
|
+
token.start,
|
|
2662
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
2663
|
+
);
|
|
2664
|
+
const role = attr(token, "role");
|
|
2665
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
2666
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
2667
|
+
const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
|
|
2668
|
+
const supportedStyles = definition?.styles;
|
|
2669
|
+
const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
|
|
2670
|
+
if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
|
|
2671
|
+
addDiagnostic(
|
|
2672
|
+
diagnostics,
|
|
2673
|
+
source,
|
|
2674
|
+
token.start,
|
|
2675
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
2676
|
+
severity,
|
|
2677
|
+
"azure-unsupported-style"
|
|
2678
|
+
);
|
|
2679
|
+
if (style && voiceName && !definition && severity)
|
|
2680
|
+
addDiagnostic(
|
|
2681
|
+
diagnostics,
|
|
2682
|
+
source,
|
|
2683
|
+
token.start,
|
|
2684
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
2685
|
+
severity
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
if (name === "say-as" || name === "sayas") {
|
|
2689
|
+
const interpretAs = attr(token, "interpret-as");
|
|
2690
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
2691
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
2692
|
+
}
|
|
2693
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
2694
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
2695
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
2696
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
2697
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
2698
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
2699
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
2700
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
2701
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
2702
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
2703
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
2704
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
2705
|
+
if (name === "lexicon") {
|
|
2706
|
+
const uri = attr(token, "uri");
|
|
2707
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
2708
|
+
else {
|
|
2709
|
+
try {
|
|
2710
|
+
const parsed = new URL(uri);
|
|
2711
|
+
if (parsed.protocol !== "https:")
|
|
2712
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
2713
|
+
} catch {
|
|
2714
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (name === "mstts:silence") {
|
|
2719
|
+
const type = attr(token, "type");
|
|
2720
|
+
const value = attr(token, "value");
|
|
2721
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
2722
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
2723
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
2724
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
2725
|
+
}
|
|
2726
|
+
if (name === "mstts:audioduration") {
|
|
2727
|
+
const value = attr(token, "value");
|
|
2728
|
+
if (!value || !isValidAzureAudioDuration(value))
|
|
2729
|
+
addDiagnostic(
|
|
2730
|
+
diagnostics,
|
|
2731
|
+
source,
|
|
2732
|
+
token.start,
|
|
2733
|
+
'<mstts:audioduration> requires a positive duration such as "10s", "5000ms", or "00:00:10".'
|
|
2734
|
+
);
|
|
2735
|
+
if (!token.selfClosing)
|
|
2736
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:audioduration> must be self-closing.");
|
|
2737
|
+
}
|
|
2738
|
+
if (name === "mstts:viseme") {
|
|
2739
|
+
const type = attr(token, "type");
|
|
2740
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
2741
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
2742
|
+
}
|
|
2743
|
+
if (name === "audio") {
|
|
2744
|
+
validateAudioSource(token, source, diagnostics, options, "audio");
|
|
2745
|
+
}
|
|
2746
|
+
if (name === "mstts:turn") {
|
|
2747
|
+
if (!attr(token, "voice")?.trim())
|
|
2748
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
|
|
2749
|
+
if (token.parentName?.toLowerCase() !== "mstts:dialog")
|
|
2750
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
|
|
2751
|
+
}
|
|
2752
|
+
if (name === "mstts:backgroundaudio") {
|
|
2753
|
+
validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
|
|
2754
|
+
const volume = attr(token, "volume");
|
|
2755
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
|
|
2756
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
|
|
2757
|
+
for (const [attribute, value] of [
|
|
2758
|
+
["fadein", attr(token, "fadein")],
|
|
2759
|
+
["fadeout", attr(token, "fadeout")]
|
|
2760
|
+
]) {
|
|
2761
|
+
if (value && !isValidAzureAudioDuration(value))
|
|
2762
|
+
addDiagnostic(
|
|
2763
|
+
diagnostics,
|
|
2764
|
+
source,
|
|
2765
|
+
token.start,
|
|
2766
|
+
`<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
if (!token.selfClosing)
|
|
2770
|
+
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
2774
|
+
const diagnostics = [];
|
|
2775
|
+
if (typeof ssml !== "string") {
|
|
2776
|
+
return [
|
|
2777
|
+
{
|
|
2778
|
+
line: 1,
|
|
2779
|
+
column: 1,
|
|
2780
|
+
message: "SSML input must be a string",
|
|
2781
|
+
severity: "error",
|
|
2782
|
+
source: "ssml-static-validator"
|
|
2783
|
+
}
|
|
2784
|
+
];
|
|
2785
|
+
}
|
|
2786
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
2787
|
+
if (ssml.length > maxLength)
|
|
2788
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
2789
|
+
try {
|
|
2790
|
+
parseSsml(ssml);
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
2793
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
2794
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
2795
|
+
return diagnostics;
|
|
2796
|
+
}
|
|
2797
|
+
const tokens = tokenizeElements(ssml);
|
|
2798
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
2799
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
2800
|
+
if (!speak || voices.length === 0)
|
|
2801
|
+
addDiagnostic(
|
|
2802
|
+
diagnostics,
|
|
2803
|
+
ssml,
|
|
2804
|
+
speak?.start ?? 0,
|
|
2805
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
2806
|
+
);
|
|
2807
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
2808
|
+
const voiceCatalog = normalizeVoiceCatalog(options);
|
|
2809
|
+
const normalizeLanguage = createLanguageNormalizer(options);
|
|
2810
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
2811
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
2812
|
+
for (const token of voicesToValidate) {
|
|
2813
|
+
const name = attr(token, "name")?.trim();
|
|
2814
|
+
const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
|
|
2815
|
+
const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
|
|
2816
|
+
if (name && !definition && policySeverity)
|
|
2817
|
+
addDiagnostic(
|
|
2818
|
+
diagnostics,
|
|
2819
|
+
ssml,
|
|
2820
|
+
token.start,
|
|
2821
|
+
`Unknown voice "${name}" is not registered in the voice catalog.`,
|
|
2822
|
+
policySeverity,
|
|
2823
|
+
"azure-unknown-voice"
|
|
2824
|
+
);
|
|
2825
|
+
if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
|
|
2826
|
+
addDiagnostic(
|
|
2827
|
+
diagnostics,
|
|
2828
|
+
ssml,
|
|
2829
|
+
token.start,
|
|
2830
|
+
`Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
|
|
2831
|
+
"warning",
|
|
2832
|
+
"azure-locale-mismatch"
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
for (const token of tokens) {
|
|
2836
|
+
const tokenName = token.name.toLowerCase();
|
|
2837
|
+
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
2838
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
2839
|
+
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
2840
|
+
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
2841
|
+
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
2842
|
+
addDiagnostic(
|
|
2843
|
+
diagnostics,
|
|
2844
|
+
ssml,
|
|
2845
|
+
token.start,
|
|
2846
|
+
`Voice "${tokenVoiceName}" does not support model "${options.model}" according to the configured feature matrix.`,
|
|
2847
|
+
"error",
|
|
2848
|
+
"azure-unsupported-model-for-voice"
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
return diagnostics;
|
|
2853
|
+
}
|
|
2854
|
+
var AZURE_VOICE_CATALOG_METADATA = {
|
|
2855
|
+
apiVersion: "2025-10-01",
|
|
2856
|
+
generatedAt: "2026-08-28T00:00:00.000Z",
|
|
2857
|
+
regions: [],
|
|
2858
|
+
voiceCount: AZURE_VOICE_DEFINITIONS.length
|
|
2859
|
+
};
|
|
2160
2860
|
|
|
2161
2861
|
// packages/ssml-editor-react/src/clearSsmlDocument.ts
|
|
2162
2862
|
function getDocumentChildren(document) {
|
|
@@ -2281,7 +2981,7 @@ function validateEntityReferences(value) {
|
|
|
2281
2981
|
index = semicolon + 1;
|
|
2282
2982
|
}
|
|
2283
2983
|
}
|
|
2284
|
-
function
|
|
2984
|
+
function findTagEnd3(source, start, hasInternalSubset = false) {
|
|
2285
2985
|
let quote;
|
|
2286
2986
|
let subsetDepth = 0;
|
|
2287
2987
|
for (let index = start + 1; index < source.length; index += 1) {
|
|
@@ -2454,7 +3154,7 @@ function parseXml(source) {
|
|
|
2454
3154
|
continue;
|
|
2455
3155
|
}
|
|
2456
3156
|
if (source.startsWith("</", index)) {
|
|
2457
|
-
const end2 =
|
|
3157
|
+
const end2 = findTagEnd3(source, index);
|
|
2458
3158
|
const name2 = parseClosingTag(source, index, end2);
|
|
2459
3159
|
const element = currentElement(stack);
|
|
2460
3160
|
if (element === void 0 || element.name !== name2) {
|
|
@@ -2466,7 +3166,7 @@ function parseXml(source) {
|
|
|
2466
3166
|
continue;
|
|
2467
3167
|
}
|
|
2468
3168
|
if (source.startsWith("<!", index)) {
|
|
2469
|
-
const end2 =
|
|
3169
|
+
const end2 = findTagEnd3(source, index, true);
|
|
2470
3170
|
parseDeclaration(source, index, end2);
|
|
2471
3171
|
appendNode2(currentElement(stack)?.children ?? children, {
|
|
2472
3172
|
kind: "declaration",
|
|
@@ -2475,7 +3175,7 @@ function parseXml(source) {
|
|
|
2475
3175
|
index = end2 + 1;
|
|
2476
3176
|
continue;
|
|
2477
3177
|
}
|
|
2478
|
-
const end =
|
|
3178
|
+
const end = findTagEnd3(source, index);
|
|
2479
3179
|
const { name, selfClosing } = parseStartTag(source, index, end);
|
|
2480
3180
|
if (stack.length === 0) {
|
|
2481
3181
|
if (root !== void 0) {
|
|
@@ -2942,6 +3642,24 @@ var SSML_COMPLETION_SNIPPETS = [
|
|
|
2942
3642
|
label: "mstts:audioduration",
|
|
2943
3643
|
insertText: '<mstts:audioduration value="10s" />'
|
|
2944
3644
|
},
|
|
3645
|
+
{
|
|
3646
|
+
label: "mstts:dialog",
|
|
3647
|
+
insertText: `<mstts:dialog>
|
|
3648
|
+
<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
|
|
3649
|
+
</mstts:dialog>`
|
|
3650
|
+
},
|
|
3651
|
+
{
|
|
3652
|
+
label: "mstts:turn",
|
|
3653
|
+
insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
|
|
3654
|
+
},
|
|
3655
|
+
{
|
|
3656
|
+
label: "mstts:backgroundaudio",
|
|
3657
|
+
insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
label: "mstts:ttsembedding",
|
|
3661
|
+
insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
|
|
3662
|
+
},
|
|
2945
3663
|
{
|
|
2946
3664
|
label: "sub",
|
|
2947
3665
|
insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
|
|
@@ -3312,6 +4030,63 @@ var SSML_TAG_DEFINITIONS = [
|
|
|
3312
4030
|
example: "10s"
|
|
3313
4031
|
}
|
|
3314
4032
|
]
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
name: "mstts:dialog",
|
|
4036
|
+
description: "Groups multiple Azure dialog turns that can use different voices.",
|
|
4037
|
+
parameters: []
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
name: "mstts:turn",
|
|
4041
|
+
description: "Adds one dialog turn using the required Azure voice name.",
|
|
4042
|
+
parameters: [
|
|
4043
|
+
{
|
|
4044
|
+
name: "voice",
|
|
4045
|
+
description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
|
|
4046
|
+
example: "en-US-JennyNeural"
|
|
4047
|
+
}
|
|
4048
|
+
]
|
|
4049
|
+
},
|
|
4050
|
+
{
|
|
4051
|
+
name: "mstts:backgroundaudio",
|
|
4052
|
+
description: "Plays background audio while speech is synthesized.",
|
|
4053
|
+
parameters: [
|
|
4054
|
+
{
|
|
4055
|
+
name: "src",
|
|
4056
|
+
description: "An absolute HTTP(S) URL for the background audio file.",
|
|
4057
|
+
example: "https://example.com/music.mp3"
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
name: "volume",
|
|
4061
|
+
description: "The background audio volume, for example `-3dB` or `medium`.",
|
|
4062
|
+
example: "-3dB"
|
|
4063
|
+
},
|
|
4064
|
+
{
|
|
4065
|
+
name: "fadein",
|
|
4066
|
+
description: "The fade-in duration, for example `1s`.",
|
|
4067
|
+
example: "1s"
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
name: "fadeout",
|
|
4071
|
+
description: "The fade-out duration, for example `500ms`.",
|
|
4072
|
+
example: "500ms"
|
|
4073
|
+
}
|
|
4074
|
+
]
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
name: "mstts:ttsembedding",
|
|
4078
|
+
description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
|
|
4079
|
+
parameters: []
|
|
4080
|
+
},
|
|
4081
|
+
{
|
|
4082
|
+
name: "mstts:embedding",
|
|
4083
|
+
description: "Specifies embedding metadata for custom voice scenarios.",
|
|
4084
|
+
parameters: []
|
|
4085
|
+
},
|
|
4086
|
+
{
|
|
4087
|
+
name: "mstts:voiceconversion",
|
|
4088
|
+
description: "Specifies voice conversion metadata for custom voice scenarios.",
|
|
4089
|
+
parameters: []
|
|
3315
4090
|
}
|
|
3316
4091
|
];
|
|
3317
4092
|
var definitionsByName = /* @__PURE__ */ new Map();
|
|
@@ -3370,7 +4145,7 @@ function toRange(source, token) {
|
|
|
3370
4145
|
function containsOffset(token, offset) {
|
|
3371
4146
|
return offset >= token.start && offset < token.end;
|
|
3372
4147
|
}
|
|
3373
|
-
function
|
|
4148
|
+
function findTagEnd4(source, start) {
|
|
3374
4149
|
let quote;
|
|
3375
4150
|
for (let index = start; index < source.length; index += 1) {
|
|
3376
4151
|
const character = source[index];
|
|
@@ -3501,7 +4276,7 @@ function findTagAtOffset(source, offset) {
|
|
|
3501
4276
|
searchStart = tokenEnd2;
|
|
3502
4277
|
continue;
|
|
3503
4278
|
}
|
|
3504
|
-
const tagEnd =
|
|
4279
|
+
const tagEnd = findTagEnd4(source, start + 1);
|
|
3505
4280
|
const contentEnd = tagEnd ?? source.length;
|
|
3506
4281
|
const tokenEnd = tagEnd === void 0 ? source.length : tagEnd + 1;
|
|
3507
4282
|
if (offset < tokenEnd) {
|
|
@@ -3650,6 +4425,7 @@ export {
|
|
|
3650
4425
|
parseSsml,
|
|
3651
4426
|
buildPartialSsml,
|
|
3652
4427
|
validateSsml,
|
|
4428
|
+
validateAzureSsml,
|
|
3653
4429
|
clearSsmlDocument,
|
|
3654
4430
|
formatXmlFragment,
|
|
3655
4431
|
getEditableRegion,
|
|
@@ -3663,4 +4439,4 @@ export {
|
|
|
3663
4439
|
findSsmlHoverTarget,
|
|
3664
4440
|
formatSsmlHover
|
|
3665
4441
|
};
|
|
3666
|
-
//# sourceMappingURL=chunk-
|
|
4442
|
+
//# sourceMappingURL=chunk-JNJVTEL6.mjs.map
|