ssml-builder-js 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/elements.mjs CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  registerSsmlCompletionProvider,
34
34
  resolveExpressAsStyles,
35
35
  updateEditableText
36
- } from "./chunk-GF4QZHLJ.mjs";
36
+ } from "./chunk-QJTVHANI.mjs";
37
37
  import "./chunk-6S5ODO6A.mjs";
38
38
 
39
39
  // packages/ssml-editor-react/src/ssmlInsertions.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { AudioElement, AzureSsmlValidationOptions, AzureValidationOptions, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsSilenceElement, MsttsVisemeElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDocument, SsmlElement, SsmlElementBase, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlText, SsmlTextNodeContext, SsmlValidationError, SsmlVoiceElement, SubElement, VoiceElement, WordElement, buildPartialSsml, buildSsml, extractSsmlText, mapSsmlTextNodes, parseSsml, validateAzureSsml, validateSsml } from './core.mjs';
1
+ export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsSilenceElement, MsttsVisemeElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDocument, SsmlElement, SsmlElementBase, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlText, SsmlTextNodeContext, SsmlValidationError, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml } from './core.mjs';
2
2
 
3
3
  interface TtsConfig {
4
4
  signal?: AbortSignal;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AudioElement, AzureSsmlValidationOptions, AzureValidationOptions, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsSilenceElement, MsttsVisemeElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDocument, SsmlElement, SsmlElementBase, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlText, SsmlTextNodeContext, SsmlValidationError, SsmlVoiceElement, SubElement, VoiceElement, WordElement, buildPartialSsml, buildSsml, extractSsmlText, mapSsmlTextNodes, parseSsml, validateAzureSsml, validateSsml } from './core.js';
1
+ export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsSilenceElement, MsttsVisemeElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDocument, SsmlElement, SsmlElementBase, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlText, SsmlTextNodeContext, SsmlValidationError, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml } from './core.js';
2
2
 
3
3
  interface TtsConfig {
4
4
  signal?: AbortSignal;
package/dist/index.js CHANGED
@@ -40,10 +40,12 @@ __export(src_exports, {
40
40
  AzureTtsClient: () => AzureTtsClient,
41
41
  AzureTtsError: () => AzureTtsError,
42
42
  AzureTtsSdkError: () => AzureTtsSdkError,
43
+ areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
43
44
  buildPartialSsml: () => buildPartialSsml,
44
45
  buildSsml: () => buildSsml,
45
46
  extractSsmlText: () => extractSsmlText,
46
47
  mapSsmlTextNodes: () => mapSsmlTextNodes,
48
+ normalizeAzureLanguage: () => normalizeAzureLanguage,
47
49
  parseSsml: () => parseSsml,
48
50
  synthesizeSpeech: () => synthesizeSpeech,
49
51
  validateAzureSsml: () => validateAzureSsml,
@@ -1241,51 +1243,123 @@ function location(source, offset) {
1241
1243
  const line = before.split("\n").length;
1242
1244
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
1243
1245
  }
1244
- function addDiagnostic(diagnostics, source, offset, message, severity = "error") {
1245
- diagnostics.push({ ...location(source, offset), message, severity });
1246
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
1247
+ diagnostics.push({ ...location(source, offset), message, severity, ...code ? { code } : {} });
1246
1248
  }
1247
1249
  function attr(token, name) {
1248
1250
  return token.attributes.get(name.toLowerCase());
1249
1251
  }
1250
- function normalizeVoiceStyleMap(customVoiceStyleMap) {
1251
- const map = new Map(
1252
- Object.entries(EXPRESS_AS_STYLES).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
1253
- );
1254
- for (const [voiceName, styles] of Object.entries(customVoiceStyleMap ?? {})) {
1255
- map.set(
1256
- voiceName.toLowerCase(),
1257
- styles.map((style) => style.toLowerCase())
1258
- );
1252
+ var ADDITIONAL_VOICE_DEFINITIONS = [
1253
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" },
1254
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
1255
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
1256
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1257
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
1258
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
1259
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" }
1260
+ ];
1261
+ var DEFAULT_LANGUAGE_ALIASES = {
1262
+ "zh-CN": ["zh-Hans"],
1263
+ "zh-TW": ["zh-Hant"]
1264
+ };
1265
+ function canonicalLanguageTag(language) {
1266
+ const trimmed = language.trim();
1267
+ if (!trimmed) return "";
1268
+ try {
1269
+ return new Intl.Locale(trimmed).toString().toLowerCase();
1270
+ } catch {
1271
+ return trimmed.toLowerCase();
1259
1272
  }
1260
- return map;
1261
1273
  }
1262
- function diagnosticSeverity(policy) {
1263
- if (policy === "ignore") return void 0;
1264
- return policy === "error" ? "error" : "warning";
1274
+ function createLanguageNormalizer(options) {
1275
+ const aliases = /* @__PURE__ */ new Map();
1276
+ const addAliasGroup = (canonical, values) => {
1277
+ const normalizedCanonical = canonicalLanguageTag(canonical);
1278
+ if (!normalizedCanonical) return;
1279
+ aliases.set(normalizedCanonical, normalizedCanonical);
1280
+ for (const value of values) {
1281
+ const normalizedValue = canonicalLanguageTag(value);
1282
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
1283
+ }
1284
+ };
1285
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
1286
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
1287
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
1288
+ return (language) => {
1289
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
1290
+ const normalized = canonicalLanguageTag(customValue);
1291
+ return aliases.get(normalized) ?? normalized;
1292
+ };
1293
+ }
1294
+ function normalizeAzureLanguage(language, options = {}) {
1295
+ return createLanguageNormalizer(options)(language);
1296
+ }
1297
+ function areAzureLanguagesEquivalent(first, second, options = {}) {
1298
+ const normalize = createLanguageNormalizer(options);
1299
+ const normalizedFirst = normalize(first);
1300
+ const normalizedSecond = normalize(second);
1301
+ if (normalizedFirst === normalizedSecond) return true;
1302
+ return normalizedFirst === languagePart(normalizedFirst) && languagePart(normalizedFirst) === languagePart(normalizedSecond);
1265
1303
  }
1266
1304
  function voiceLocalePrefix(voiceName) {
1267
1305
  const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
1268
1306
  if (!match?.groups) return void 0;
1307
+ const tag = `${match.groups.language}-${match.groups.region}`;
1269
1308
  return {
1270
1309
  language: match.groups.language.toLowerCase(),
1271
- region: match.groups.region.toLowerCase()
1310
+ region: match.groups.region.toLowerCase(),
1311
+ tag
1272
1312
  };
1273
1313
  }
1274
- function languageLocalePrefix(language) {
1275
- const match = /^(?<language>[A-Za-z]{2,3})(?:-(?<region>[A-Za-z]{2}|\d{3}))?(?:-|$)/.exec(language.trim());
1276
- if (!match?.groups) return void 0;
1314
+ function definitionFromStyleMap(voiceName, styles) {
1277
1315
  return {
1278
- language: match.groups.language.toLowerCase(),
1279
- region: match.groups.region?.toLowerCase()
1316
+ name: voiceName,
1317
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
1318
+ styles
1280
1319
  };
1281
1320
  }
1282
- function voiceMatchesLanguage(voiceName, language) {
1283
- const voiceLocale = voiceLocalePrefix(voiceName);
1284
- const languageLocale = languageLocalePrefix(language);
1285
- if (!voiceLocale || !languageLocale) return void 0;
1286
- return voiceLocale.language === languageLocale.language && (languageLocale.region === void 0 || voiceLocale.region === languageLocale.region);
1321
+ function normalizeVoiceCatalog(options) {
1322
+ const definitions = /* @__PURE__ */ new Map();
1323
+ for (const [name, styles] of Object.entries(EXPRESS_AS_STYLES)) {
1324
+ definitions.set(name.toLowerCase(), definitionFromStyleMap(name, styles));
1325
+ }
1326
+ for (const definition of ADDITIONAL_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
1327
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
1328
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
1329
+ for (const definition of options.customVoiceDefinitions ?? [])
1330
+ definitions.set(definition.name.toLowerCase(), definition);
1331
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
1332
+ const key = voiceName.toLowerCase();
1333
+ const current = definitions.get(key);
1334
+ definitions.set(key, {
1335
+ ...current ?? definitionFromStyleMap(voiceName, styles),
1336
+ name: current?.name ?? voiceName,
1337
+ styles: styles.map((style) => style.toLowerCase())
1338
+ });
1339
+ }
1340
+ return definitions;
1341
+ }
1342
+ function diagnosticSeverity(policy) {
1343
+ if (policy === "ignore") return void 0;
1344
+ return policy === "error" ? "error" : "warning";
1287
1345
  }
1288
- function validateElement(token, source, diagnostics, voiceName, options, voiceStyleMap) {
1346
+ function languagePart(language) {
1347
+ try {
1348
+ return new Intl.Locale(language).language.toLowerCase();
1349
+ } catch {
1350
+ return language.split("-")[0]?.toLowerCase() ?? "";
1351
+ }
1352
+ }
1353
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
1354
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
1355
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
1356
+ const normalizedLanguage = normalizeLanguage(language);
1357
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
1358
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
1359
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
1360
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
1361
+ }
1362
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1289
1363
  const name = token.name.toLowerCase();
1290
1364
  if (name === "voice" && !attr(token, "name")?.trim())
1291
1365
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
@@ -1327,17 +1401,19 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceSt
1327
1401
  const role = attr(token, "role");
1328
1402
  if (role && !ALLOWED_ROLES.has(role))
1329
1403
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
1330
- const supportedStyles = voiceName ? voiceStyleMap.get(voiceName.toLowerCase()) : void 0;
1331
- const severity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
1332
- if (style && supportedStyles && !supportedStyles.includes(style.toLowerCase()) && severity)
1404
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
1405
+ const supportedStyles = definition?.styles;
1406
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
1407
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
1333
1408
  addDiagnostic(
1334
1409
  diagnostics,
1335
1410
  source,
1336
1411
  token.start,
1337
1412
  `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
1338
- severity
1413
+ severity,
1414
+ "azure-unsupported-style"
1339
1415
  );
1340
- if (style && voiceName && !supportedStyles && severity)
1416
+ if (style && voiceName && !definition && severity)
1341
1417
  addDiagnostic(
1342
1418
  diagnostics,
1343
1419
  source,
@@ -1442,32 +1518,36 @@ function validateAzureSsml(ssml, options = {}) {
1442
1518
  "Azure SSML requires at least one <voice> element under <speak>."
1443
1519
  );
1444
1520
  const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
1445
- const voiceStyleMap = normalizeVoiceStyleMap(options.customVoiceStyleMap);
1521
+ const voiceCatalog = normalizeVoiceCatalog(options);
1522
+ const normalizeLanguage = createLanguageNormalizer(options);
1446
1523
  const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
1447
1524
  const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
1448
1525
  for (const token of voicesToValidate) {
1449
1526
  const name = attr(token, "name")?.trim();
1450
1527
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1451
- if (name && !voiceStyleMap.has(name.toLowerCase()) && policySeverity)
1528
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
1529
+ if (name && !definition && policySeverity)
1452
1530
  addDiagnostic(
1453
1531
  diagnostics,
1454
1532
  ssml,
1455
1533
  token.start,
1456
- `Unknown voice "${name}" is not registered in the voice style map.`,
1457
- policySeverity
1534
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
1535
+ policySeverity,
1536
+ "azure-unknown-voice"
1458
1537
  );
1459
- if (name && language && voiceMatchesLanguage(name, language) === false)
1538
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
1460
1539
  addDiagnostic(
1461
1540
  diagnostics,
1462
1541
  ssml,
1463
1542
  token.start,
1464
1543
  `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
1465
- "warning"
1544
+ "warning",
1545
+ "azure-locale-mismatch"
1466
1546
  );
1467
1547
  }
1468
1548
  for (const token of tokens) {
1469
1549
  const tokenVoiceName = options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
1470
- validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceStyleMap);
1550
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
1471
1551
  }
1472
1552
  return diagnostics;
1473
1553
  }
@@ -1658,10 +1738,12 @@ _options = new WeakMap();
1658
1738
  AzureTtsClient,
1659
1739
  AzureTtsError,
1660
1740
  AzureTtsSdkError,
1741
+ areAzureLanguagesEquivalent,
1661
1742
  buildPartialSsml,
1662
1743
  buildSsml,
1663
1744
  extractSsmlText,
1664
1745
  mapSsmlTextNodes,
1746
+ normalizeAzureLanguage,
1665
1747
  parseSsml,
1666
1748
  synthesizeSpeech,
1667
1749
  validateAzureSsml,