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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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) => ({ "&amp;": "&", "&apos;": "'", "&gt;": ">", "&lt;": "<", "&quot;": '"' })[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 findTagEnd2(source, start, hasInternalSubset = false) {
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 = findTagEnd2(source, index);
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 = findTagEnd2(source, index, true);
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 = findTagEnd2(source, index);
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 findTagEnd3(source, start) {
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 = findTagEnd3(source, start + 1);
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-MWSDFGXW.mjs.map
4442
+ //# sourceMappingURL=chunk-JNJVTEL6.mjs.map