ssml-builder-js 2.9.0 → 2.11.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.
@@ -53,6 +53,7 @@ var SSML_ATTRS = {
53
53
  MSTTS_XMLNS: "xmlns:mstts",
54
54
  NAME: "name",
55
55
  VOICE: "voice",
56
+ SPEAKER: "speaker",
56
57
  EFFECT: "effect",
57
58
  RATE: "rate",
58
59
  PITCH: "pitch",
@@ -83,6 +84,11 @@ var SSML_ATTRS = {
83
84
  ALIAS: "alias",
84
85
  MARK: "mark",
85
86
  URI: "uri",
87
+ ID: "id",
88
+ MODEL: "model",
89
+ PROFILE: "profile",
90
+ URL: "url",
91
+ SPEAKER_PROFILE_ID: "speakerProfileId",
86
92
  TYPE: "type",
87
93
  VALUE: "value",
88
94
  FADE_IN: "fadein",
@@ -180,6 +186,7 @@ function getAttributes(element) {
180
186
  break;
181
187
  case SSML_TAGS.MSTTS_TURN:
182
188
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
189
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
183
190
  break;
184
191
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
185
192
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -188,9 +195,18 @@ function getAttributes(element) {
188
195
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
189
196
  break;
190
197
  case SSML_TAGS.MSTTS_DIALOG:
198
+ break;
191
199
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
200
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
201
+ break;
192
202
  case SSML_TAGS.MSTTS_EMBEDDING:
203
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
204
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
205
+ break;
193
206
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
207
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
208
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
209
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
194
210
  break;
195
211
  case SSML_TAGS.PARAGRAPH:
196
212
  case SSML_TAGS.SENTENCE:
@@ -765,7 +781,9 @@ function convertElement(node) {
765
781
  case SSML_TAGS.MSTTS_TURN: {
766
782
  const element = { type: SSML_TAGS.MSTTS_TURN };
767
783
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
784
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
768
785
  if (voice !== void 0) element.voice = voice;
786
+ if (speaker !== void 0) element.speaker = speaker;
769
787
  return finishElement(element, node, attributes);
770
788
  }
771
789
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -782,14 +800,26 @@ function convertElement(node) {
782
800
  }
783
801
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
784
802
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
803
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
804
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
785
805
  return finishElement(element, node, attributes);
786
806
  }
787
807
  case SSML_TAGS.MSTTS_EMBEDDING: {
788
808
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
809
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
810
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
811
+ if (id !== void 0) element.id = id;
812
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
789
813
  return finishElement(element, node, attributes);
790
814
  }
791
815
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
792
816
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
817
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
818
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
819
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
820
+ if (url !== void 0) element.url = url;
821
+ if (profile !== void 0) element.profile = profile;
822
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
793
823
  return finishElement(element, node, attributes);
794
824
  }
795
825
  default: {
@@ -1165,6 +1195,7 @@ function elementAttributes(element) {
1165
1195
  break;
1166
1196
  case "mstts:turn":
1167
1197
  addAttribute2(attributes, "voice", element.voice);
1198
+ addAttribute2(attributes, "speaker", element.speaker);
1168
1199
  break;
1169
1200
  case "mstts:backgroundaudio":
1170
1201
  addAttribute2(attributes, "src", element.src);
@@ -1172,6 +1203,18 @@ function elementAttributes(element) {
1172
1203
  addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
1173
1204
  addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
1174
1205
  break;
1206
+ case "mstts:ttsembedding":
1207
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1208
+ break;
1209
+ case "mstts:embedding":
1210
+ addAttribute2(attributes, "id", element.id);
1211
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1212
+ break;
1213
+ case "mstts:voiceconversion":
1214
+ addAttribute2(attributes, "url", element.url);
1215
+ addAttribute2(attributes, "profile", element.profile);
1216
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1217
+ break;
1175
1218
  }
1176
1219
  return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
1177
1220
  }
@@ -1403,6 +1446,9 @@ var AZURE_VOICE_DEFINITIONS = [
1403
1446
  },
1404
1447
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
1405
1448
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1449
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
1450
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
1451
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
1406
1452
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1407
1453
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1408
1454
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -1499,6 +1545,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1499
1545
  "Enumerationcomma"
1500
1546
  ]);
1501
1547
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1548
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
1549
+ function featureStatusForTag(name, options) {
1550
+ const tagName = canonicalTagName(name);
1551
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
1552
+ ([candidate]) => canonicalTagName(candidate) === tagName
1553
+ )?.[1];
1554
+ if (configured) return configured;
1555
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
1556
+ return "preview";
1557
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
1558
+ return void 0;
1559
+ }
1502
1560
  function decodeAttribute(value) {
1503
1561
  return value.replace(
1504
1562
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -1558,11 +1616,14 @@ function tokenizeElements(source) {
1558
1616
  }
1559
1617
  const selfClosing = /\/\s*>$/.test(raw);
1560
1618
  const parent = openElements[openElements.length - 1];
1619
+ const childElementIndex = parent?.childElementCount;
1620
+ if (parent) parent.childElementCount += 1;
1561
1621
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1562
1622
  const tokenName = nameMatch[1];
1563
1623
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1564
1624
  tokens.push({
1565
1625
  attributes,
1626
+ childElementIndex,
1566
1627
  end,
1567
1628
  name: tokenName,
1568
1629
  parentName: parent?.name,
@@ -1572,6 +1633,7 @@ function tokenizeElements(source) {
1572
1633
  });
1573
1634
  if (!selfClosing) {
1574
1635
  openElements.push({
1636
+ childElementCount: 0,
1575
1637
  name: tokenName,
1576
1638
  voiceName: tokenVoiceName
1577
1639
  });
@@ -1610,6 +1672,12 @@ function isValidAzureAudioDuration(value) {
1610
1672
  if (!clock) return false;
1611
1673
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1612
1674
  }
1675
+ function isValidAzureBackgroundAudioDuration(value) {
1676
+ const match = /^(\d+)$/.exec(value.trim());
1677
+ if (!match) return false;
1678
+ const milliseconds = Number(match[1]);
1679
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1680
+ }
1613
1681
  function attr(token, name) {
1614
1682
  return token.attributes.get(name.toLowerCase());
1615
1683
  }
@@ -1749,9 +1817,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1749
1817
  }
1750
1818
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1751
1819
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1752
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1820
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1821
+ try {
1822
+ return new URL(allowedOrigin).origin === parsed.origin;
1823
+ } catch {
1824
+ return allowedOrigin === parsed.origin;
1825
+ }
1826
+ }) ?? false;
1827
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1753
1828
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1754
- else if (!options.allowExternalAudio)
1829
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1755
1830
  addDiagnostic(
1756
1831
  diagnostics,
1757
1832
  source,
@@ -1761,6 +1836,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1761
1836
  }
1762
1837
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1763
1838
  const name = token.name.toLowerCase();
1839
+ const tagStatus = featureStatusForTag(token.name, options);
1840
+ if (tagStatus === "preview")
1841
+ addDiagnostic(
1842
+ diagnostics,
1843
+ source,
1844
+ token.start,
1845
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
1846
+ "warning",
1847
+ "azure-preview-tag"
1848
+ );
1849
+ if (tagStatus === "deprecated")
1850
+ addDiagnostic(
1851
+ diagnostics,
1852
+ source,
1853
+ token.start,
1854
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
1855
+ "info",
1856
+ "azure-deprecated-tag"
1857
+ );
1764
1858
  if (name === "voice" && !attr(token, "name")?.trim())
1765
1859
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1766
1860
  if (name === "break") {
@@ -1881,28 +1975,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1881
1975
  validateAudioSource(token, source, diagnostics, options, "audio");
1882
1976
  }
1883
1977
  if (name === "mstts:turn") {
1884
- if (!attr(token, "voice")?.trim())
1885
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1978
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
1979
+ addDiagnostic(
1980
+ diagnostics,
1981
+ source,
1982
+ token.start,
1983
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
1984
+ );
1886
1985
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1887
1986
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1888
1987
  }
1889
1988
  if (name === "mstts:backgroundaudio") {
1890
1989
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1891
1990
  const volume = attr(token, "volume");
1892
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
1991
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1893
1992
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1894
1993
  for (const [attribute, value] of [
1895
1994
  ["fadein", attr(token, "fadein")],
1896
1995
  ["fadeout", attr(token, "fadeout")]
1897
1996
  ]) {
1898
- if (value && !isValidAzureAudioDuration(value))
1997
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1899
1998
  addDiagnostic(
1900
1999
  diagnostics,
1901
2000
  source,
1902
2001
  token.start,
1903
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2002
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1904
2003
  );
1905
2004
  }
2005
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2006
+ addDiagnostic(
2007
+ diagnostics,
2008
+ source,
2009
+ token.start,
2010
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2011
+ );
1906
2012
  if (!token.selfClosing)
1907
2013
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1908
2014
  }
@@ -1934,6 +2040,16 @@ function validateAzureSsml(ssml, options = {}) {
1934
2040
  const tokens = tokenizeElements(ssml);
1935
2041
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1936
2042
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2043
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2044
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2045
+ if (index > 0)
2046
+ addDiagnostic(
2047
+ diagnostics,
2048
+ ssml,
2049
+ token.start,
2050
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2051
+ );
2052
+ }
1937
2053
  if (!speak || voices.length === 0)
1938
2054
  addDiagnostic(
1939
2055
  diagnostics,
@@ -1950,6 +2066,24 @@ function validateAzureSsml(ssml, options = {}) {
1950
2066
  const name = attr(token, "name")?.trim();
1951
2067
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1952
2068
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2069
+ if (name && definition?.status === "preview")
2070
+ addDiagnostic(
2071
+ diagnostics,
2072
+ ssml,
2073
+ token.start,
2074
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
2075
+ "warning",
2076
+ "azure-preview-voice"
2077
+ );
2078
+ if (name && definition?.status === "deprecated")
2079
+ addDiagnostic(
2080
+ diagnostics,
2081
+ ssml,
2082
+ token.start,
2083
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
2084
+ "info",
2085
+ "azure-deprecated-voice"
2086
+ );
1953
2087
  if (name && !definition && policySeverity)
1954
2088
  addDiagnostic(
1955
2089
  diagnostics,
@@ -2023,4 +2157,4 @@ export {
2023
2157
  getAzureVoiceCatalogMetadata,
2024
2158
  getBuiltInVoiceCatalogMetadata
2025
2159
  };
2026
- //# sourceMappingURL=chunk-7LCSH4SZ.mjs.map
2160
+ //# sourceMappingURL=chunk-SNRI43QJ.mjs.map