ssml-builder-js 2.6.0 → 2.8.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-4RQETJUI.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, SsmlDiagnosticSource, 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, SsmlDiagnosticSource, 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,
@@ -1144,7 +1146,8 @@ var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
1144
1146
  "fraction",
1145
1147
  "address",
1146
1148
  "name",
1147
- "currency"
1149
+ "currency",
1150
+ "number_digit"
1148
1151
  ]);
1149
1152
  var ALLOWED_ROLES = /* @__PURE__ */ new Set([
1150
1153
  "Girl",
@@ -1241,51 +1244,137 @@ function location(source, offset) {
1241
1244
  const line = before.split("\n").length;
1242
1245
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
1243
1246
  }
1244
- function addDiagnostic(diagnostics, source, offset, message, severity = "error") {
1245
- diagnostics.push({ ...location(source, offset), message, severity });
1247
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
1248
+ diagnostics.push({
1249
+ ...location(source, offset),
1250
+ message,
1251
+ severity,
1252
+ source: "ssml-static-validator",
1253
+ ...code ? { code } : {}
1254
+ });
1255
+ }
1256
+ function isSupportedProsodyRate(value) {
1257
+ const trimmed = value.trim();
1258
+ if (/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(trimmed)) return true;
1259
+ const multiplier = /^(\d+(?:\.\d+)?)(x)?$/i.exec(trimmed);
1260
+ if (!multiplier) return false;
1261
+ const numericValue = Number(multiplier[1]);
1262
+ return numericValue >= 0.5 && numericValue <= 2;
1246
1263
  }
1247
1264
  function attr(token, name) {
1248
1265
  return token.attributes.get(name.toLowerCase());
1249
1266
  }
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
- );
1267
+ var ADDITIONAL_VOICE_DEFINITIONS = [
1268
+ { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" },
1269
+ { name: "es-ES-ElviraNeural", locale: "es-ES" },
1270
+ { name: "th-TH-PremwadeeNeural", locale: "th-TH" },
1271
+ { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1272
+ { name: "vi-VN-HoaiMyNeural", locale: "vi-VN" },
1273
+ { name: "id-ID-GadisNeural", locale: "id-ID" },
1274
+ { name: "ms-MY-YasminNeural", locale: "ms-MY" }
1275
+ ];
1276
+ var DEFAULT_LANGUAGE_ALIASES = {
1277
+ "zh-CN": ["zh-Hans"],
1278
+ "zh-TW": ["zh-Hant"]
1279
+ };
1280
+ function canonicalLanguageTag(language) {
1281
+ const trimmed = language.trim();
1282
+ if (!trimmed) return "";
1283
+ try {
1284
+ return new Intl.Locale(trimmed).toString().toLowerCase();
1285
+ } catch {
1286
+ return trimmed.toLowerCase();
1259
1287
  }
1260
- return map;
1261
1288
  }
1262
- function diagnosticSeverity(policy) {
1263
- if (policy === "ignore") return void 0;
1264
- return policy === "error" ? "error" : "warning";
1289
+ function createLanguageNormalizer(options) {
1290
+ const aliases = /* @__PURE__ */ new Map();
1291
+ const addAliasGroup = (canonical, values) => {
1292
+ const normalizedCanonical = canonicalLanguageTag(canonical);
1293
+ if (!normalizedCanonical) return;
1294
+ aliases.set(normalizedCanonical, normalizedCanonical);
1295
+ for (const value of values) {
1296
+ const normalizedValue = canonicalLanguageTag(value);
1297
+ if (normalizedValue) aliases.set(normalizedValue, normalizedCanonical);
1298
+ }
1299
+ };
1300
+ for (const [canonical, values] of Object.entries(DEFAULT_LANGUAGE_ALIASES)) addAliasGroup(canonical, values);
1301
+ for (const [canonical, valueOrValues] of Object.entries(options.languageAliases ?? {}))
1302
+ addAliasGroup(canonical, typeof valueOrValues === "string" ? [valueOrValues] : valueOrValues);
1303
+ return (language) => {
1304
+ const customValue = options.normalizeLanguage ? options.normalizeLanguage(language) : language;
1305
+ const normalized = canonicalLanguageTag(customValue);
1306
+ return aliases.get(normalized) ?? normalized;
1307
+ };
1308
+ }
1309
+ function normalizeAzureLanguage(language, options = {}) {
1310
+ return createLanguageNormalizer(options)(language);
1311
+ }
1312
+ function areAzureLanguagesEquivalent(first, second, options = {}) {
1313
+ const normalize = createLanguageNormalizer(options);
1314
+ const normalizedFirst = normalize(first);
1315
+ const normalizedSecond = normalize(second);
1316
+ if (normalizedFirst === normalizedSecond) return true;
1317
+ return normalizedFirst === languagePart(normalizedFirst) && languagePart(normalizedFirst) === languagePart(normalizedSecond);
1265
1318
  }
1266
1319
  function voiceLocalePrefix(voiceName) {
1267
1320
  const match = /^(?<language>[A-Za-z]{2,3})-(?<region>[A-Za-z]{2}|\d{3})(?:-|$)/.exec(voiceName.trim());
1268
1321
  if (!match?.groups) return void 0;
1322
+ const tag = `${match.groups.language}-${match.groups.region}`;
1269
1323
  return {
1270
1324
  language: match.groups.language.toLowerCase(),
1271
- region: match.groups.region.toLowerCase()
1325
+ region: match.groups.region.toLowerCase(),
1326
+ tag
1272
1327
  };
1273
1328
  }
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;
1329
+ function definitionFromStyleMap(voiceName, styles) {
1277
1330
  return {
1278
- language: match.groups.language.toLowerCase(),
1279
- region: match.groups.region?.toLowerCase()
1331
+ name: voiceName,
1332
+ locale: voiceLocalePrefix(voiceName)?.tag ?? "",
1333
+ styles
1280
1334
  };
1281
1335
  }
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);
1336
+ function normalizeVoiceCatalog(options) {
1337
+ const definitions = /* @__PURE__ */ new Map();
1338
+ for (const [name, styles] of Object.entries(EXPRESS_AS_STYLES)) {
1339
+ definitions.set(name.toLowerCase(), definitionFromStyleMap(name, styles));
1340
+ }
1341
+ for (const definition of ADDITIONAL_VOICE_DEFINITIONS) definitions.set(definition.name.toLowerCase(), definition);
1342
+ for (const definition of options.voiceCatalog ?? []) definitions.set(definition.name.toLowerCase(), definition);
1343
+ for (const definition of options.voiceDefinitions ?? []) definitions.set(definition.name.toLowerCase(), definition);
1344
+ for (const definition of options.customVoiceDefinitions ?? [])
1345
+ definitions.set(definition.name.toLowerCase(), definition);
1346
+ for (const [voiceName, styles] of Object.entries(options.customVoiceStyleMap ?? {})) {
1347
+ const key = voiceName.toLowerCase();
1348
+ const current = definitions.get(key);
1349
+ definitions.set(key, {
1350
+ ...current ?? definitionFromStyleMap(voiceName, styles),
1351
+ name: current?.name ?? voiceName,
1352
+ styles: styles.map((style) => style.toLowerCase())
1353
+ });
1354
+ }
1355
+ return definitions;
1356
+ }
1357
+ function diagnosticSeverity(policy) {
1358
+ if (policy === "ignore") return void 0;
1359
+ return policy === "error" ? "error" : "warning";
1360
+ }
1361
+ function languagePart(language) {
1362
+ try {
1363
+ return new Intl.Locale(language).language.toLowerCase();
1364
+ } catch {
1365
+ return language.split("-")[0]?.toLowerCase() ?? "";
1366
+ }
1367
+ }
1368
+ function definitionMatchesLanguage(definition, voiceName, language, normalizeLanguage) {
1369
+ const candidateLanguages = definition ? [definition.locale, ...definition.secondaryLocales ?? []].filter(Boolean) : [voiceLocalePrefix(voiceName)?.tag ?? ""];
1370
+ if (candidateLanguages.length === 0 || !language.trim()) return void 0;
1371
+ const normalizedLanguage = normalizeLanguage(language);
1372
+ const normalizedCandidates = candidateLanguages.map(normalizeLanguage);
1373
+ if (normalizedCandidates.includes(normalizedLanguage)) return true;
1374
+ if (!normalizedLanguage || !normalizedCandidates.some(Boolean)) return void 0;
1375
+ return normalizedLanguage === languagePart(normalizedLanguage) ? normalizedCandidates.some((candidate) => languagePart(candidate) === normalizedLanguage) : false;
1287
1376
  }
1288
- function validateElement(token, source, diagnostics, voiceName, options, voiceStyleMap) {
1377
+ function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1289
1378
  const name = token.name.toLowerCase();
1290
1379
  if (name === "voice" && !attr(token, "name")?.trim())
1291
1380
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
@@ -1305,7 +1394,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceSt
1305
1394
  const rate = attr(token, "rate");
1306
1395
  const pitch = attr(token, "pitch");
1307
1396
  const volume = attr(token, "volume");
1308
- if (rate && !/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(rate.trim()))
1397
+ if (rate && !isSupportedProsodyRate(rate))
1309
1398
  addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
1310
1399
  if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
1311
1400
  addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
@@ -1327,17 +1416,19 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceSt
1327
1416
  const role = attr(token, "role");
1328
1417
  if (role && !ALLOWED_ROLES.has(role))
1329
1418
  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)
1419
+ const definition = voiceName ? voiceCatalog.get(voiceName.toLowerCase()) : void 0;
1420
+ const supportedStyles = definition?.styles;
1421
+ const severity = diagnosticSeverity(options.unsupportedStylePolicy ?? options.unknownVoicePolicy ?? "warn");
1422
+ if (style && definition && !supportedStyles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase()) && severity)
1333
1423
  addDiagnostic(
1334
1424
  diagnostics,
1335
1425
  source,
1336
1426
  token.start,
1337
1427
  `Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
1338
- severity
1428
+ severity,
1429
+ "azure-unsupported-style"
1339
1430
  );
1340
- if (style && voiceName && !supportedStyles && severity)
1431
+ if (style && voiceName && !definition && severity)
1341
1432
  addDiagnostic(
1342
1433
  diagnostics,
1343
1434
  source,
@@ -1418,7 +1509,15 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceSt
1418
1509
  function validateAzureSsml(ssml, options = {}) {
1419
1510
  const diagnostics = [];
1420
1511
  if (typeof ssml !== "string") {
1421
- return [{ line: 1, column: 1, message: "SSML input must be a string", severity: "error" }];
1512
+ return [
1513
+ {
1514
+ line: 1,
1515
+ column: 1,
1516
+ message: "SSML input must be a string",
1517
+ severity: "error",
1518
+ source: "ssml-static-validator"
1519
+ }
1520
+ ];
1422
1521
  }
1423
1522
  const maxLength = options.maxLength ?? 1e4;
1424
1523
  if (ssml.length > maxLength)
@@ -1442,32 +1541,36 @@ function validateAzureSsml(ssml, options = {}) {
1442
1541
  "Azure SSML requires at least one <voice> element under <speak>."
1443
1542
  );
1444
1543
  const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
1445
- const voiceStyleMap = normalizeVoiceStyleMap(options.customVoiceStyleMap);
1544
+ const voiceCatalog = normalizeVoiceCatalog(options);
1545
+ const normalizeLanguage = createLanguageNormalizer(options);
1446
1546
  const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
1447
1547
  const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
1448
1548
  for (const token of voicesToValidate) {
1449
1549
  const name = attr(token, "name")?.trim();
1450
1550
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1451
- if (name && !voiceStyleMap.has(name.toLowerCase()) && policySeverity)
1551
+ const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
1552
+ if (name && !definition && policySeverity)
1452
1553
  addDiagnostic(
1453
1554
  diagnostics,
1454
1555
  ssml,
1455
1556
  token.start,
1456
- `Unknown voice "${name}" is not registered in the voice style map.`,
1457
- policySeverity
1557
+ `Unknown voice "${name}" is not registered in the voice catalog.`,
1558
+ policySeverity,
1559
+ "azure-unknown-voice"
1458
1560
  );
1459
- if (name && language && voiceMatchesLanguage(name, language) === false)
1561
+ if (name && language && definitionMatchesLanguage(definition, name, language, normalizeLanguage) === false)
1460
1562
  addDiagnostic(
1461
1563
  diagnostics,
1462
1564
  ssml,
1463
1565
  token.start,
1464
1566
  `Voice "${name}" does not match language "${language}"; the voice name prefix indicates a different language or region.`,
1465
- "warning"
1567
+ "warning",
1568
+ "azure-locale-mismatch"
1466
1569
  );
1467
1570
  }
1468
1571
  for (const token of tokens) {
1469
1572
  const tokenVoiceName = options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
1470
- validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceStyleMap);
1573
+ validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
1471
1574
  }
1472
1575
  return diagnostics;
1473
1576
  }
@@ -1658,10 +1761,12 @@ _options = new WeakMap();
1658
1761
  AzureTtsClient,
1659
1762
  AzureTtsError,
1660
1763
  AzureTtsSdkError,
1764
+ areAzureLanguagesEquivalent,
1661
1765
  buildPartialSsml,
1662
1766
  buildSsml,
1663
1767
  extractSsmlText,
1664
1768
  mapSsmlTextNodes,
1769
+ normalizeAzureLanguage,
1665
1770
  parseSsml,
1666
1771
  synthesizeSpeech,
1667
1772
  validateAzureSsml,