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.
package/dist/core.js CHANGED
@@ -94,6 +94,7 @@ var SSML_ATTRS = {
94
94
  MSTTS_XMLNS: "xmlns:mstts",
95
95
  NAME: "name",
96
96
  VOICE: "voice",
97
+ SPEAKER: "speaker",
97
98
  EFFECT: "effect",
98
99
  RATE: "rate",
99
100
  PITCH: "pitch",
@@ -124,6 +125,11 @@ var SSML_ATTRS = {
124
125
  ALIAS: "alias",
125
126
  MARK: "mark",
126
127
  URI: "uri",
128
+ ID: "id",
129
+ MODEL: "model",
130
+ PROFILE: "profile",
131
+ URL: "url",
132
+ SPEAKER_PROFILE_ID: "speakerProfileId",
127
133
  TYPE: "type",
128
134
  VALUE: "value",
129
135
  FADE_IN: "fadein",
@@ -221,6 +227,7 @@ function getAttributes(element) {
221
227
  break;
222
228
  case SSML_TAGS.MSTTS_TURN:
223
229
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
230
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
224
231
  break;
225
232
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
226
233
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -229,9 +236,18 @@ function getAttributes(element) {
229
236
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
230
237
  break;
231
238
  case SSML_TAGS.MSTTS_DIALOG:
239
+ break;
232
240
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
241
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
242
+ break;
233
243
  case SSML_TAGS.MSTTS_EMBEDDING:
244
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
245
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
246
+ break;
234
247
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
248
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
249
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
250
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
235
251
  break;
236
252
  case SSML_TAGS.PARAGRAPH:
237
253
  case SSML_TAGS.SENTENCE:
@@ -806,7 +822,9 @@ function convertElement(node) {
806
822
  case SSML_TAGS.MSTTS_TURN: {
807
823
  const element = { type: SSML_TAGS.MSTTS_TURN };
808
824
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
825
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
809
826
  if (voice !== void 0) element.voice = voice;
827
+ if (speaker !== void 0) element.speaker = speaker;
810
828
  return finishElement(element, node, attributes);
811
829
  }
812
830
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -823,14 +841,26 @@ function convertElement(node) {
823
841
  }
824
842
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
825
843
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
844
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
845
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
826
846
  return finishElement(element, node, attributes);
827
847
  }
828
848
  case SSML_TAGS.MSTTS_EMBEDDING: {
829
849
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
850
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
851
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
852
+ if (id !== void 0) element.id = id;
853
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
830
854
  return finishElement(element, node, attributes);
831
855
  }
832
856
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
833
857
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
858
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
859
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
860
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
861
+ if (url !== void 0) element.url = url;
862
+ if (profile !== void 0) element.profile = profile;
863
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
834
864
  return finishElement(element, node, attributes);
835
865
  }
836
866
  default: {
@@ -1206,6 +1236,7 @@ function elementAttributes(element) {
1206
1236
  break;
1207
1237
  case "mstts:turn":
1208
1238
  addAttribute2(attributes, "voice", element.voice);
1239
+ addAttribute2(attributes, "speaker", element.speaker);
1209
1240
  break;
1210
1241
  case "mstts:backgroundaudio":
1211
1242
  addAttribute2(attributes, "src", element.src);
@@ -1213,6 +1244,18 @@ function elementAttributes(element) {
1213
1244
  addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
1214
1245
  addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
1215
1246
  break;
1247
+ case "mstts:ttsembedding":
1248
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1249
+ break;
1250
+ case "mstts:embedding":
1251
+ addAttribute2(attributes, "id", element.id);
1252
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1253
+ break;
1254
+ case "mstts:voiceconversion":
1255
+ addAttribute2(attributes, "url", element.url);
1256
+ addAttribute2(attributes, "profile", element.profile);
1257
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1258
+ break;
1216
1259
  }
1217
1260
  return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
1218
1261
  }
@@ -1444,6 +1487,9 @@ var AZURE_VOICE_DEFINITIONS = [
1444
1487
  },
1445
1488
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
1446
1489
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1490
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
1491
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
1492
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
1447
1493
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1448
1494
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1449
1495
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -1540,6 +1586,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1540
1586
  "Enumerationcomma"
1541
1587
  ]);
1542
1588
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1589
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
1590
+ function featureStatusForTag(name, options) {
1591
+ const tagName = canonicalTagName(name);
1592
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
1593
+ ([candidate]) => canonicalTagName(candidate) === tagName
1594
+ )?.[1];
1595
+ if (configured) return configured;
1596
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
1597
+ return "preview";
1598
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
1599
+ return void 0;
1600
+ }
1543
1601
  function decodeAttribute(value) {
1544
1602
  return value.replace(
1545
1603
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -1599,11 +1657,14 @@ function tokenizeElements(source) {
1599
1657
  }
1600
1658
  const selfClosing = /\/\s*>$/.test(raw);
1601
1659
  const parent = openElements[openElements.length - 1];
1660
+ const childElementIndex = parent?.childElementCount;
1661
+ if (parent) parent.childElementCount += 1;
1602
1662
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1603
1663
  const tokenName = nameMatch[1];
1604
1664
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1605
1665
  tokens.push({
1606
1666
  attributes,
1667
+ childElementIndex,
1607
1668
  end,
1608
1669
  name: tokenName,
1609
1670
  parentName: parent?.name,
@@ -1613,6 +1674,7 @@ function tokenizeElements(source) {
1613
1674
  });
1614
1675
  if (!selfClosing) {
1615
1676
  openElements.push({
1677
+ childElementCount: 0,
1616
1678
  name: tokenName,
1617
1679
  voiceName: tokenVoiceName
1618
1680
  });
@@ -1651,6 +1713,12 @@ function isValidAzureAudioDuration(value) {
1651
1713
  if (!clock) return false;
1652
1714
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1653
1715
  }
1716
+ function isValidAzureBackgroundAudioDuration(value) {
1717
+ const match = /^(\d+)$/.exec(value.trim());
1718
+ if (!match) return false;
1719
+ const milliseconds = Number(match[1]);
1720
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1721
+ }
1654
1722
  function attr(token, name) {
1655
1723
  return token.attributes.get(name.toLowerCase());
1656
1724
  }
@@ -1790,9 +1858,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1790
1858
  }
1791
1859
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1792
1860
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1793
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1861
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1862
+ try {
1863
+ return new URL(allowedOrigin).origin === parsed.origin;
1864
+ } catch {
1865
+ return allowedOrigin === parsed.origin;
1866
+ }
1867
+ }) ?? false;
1868
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1794
1869
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1795
- else if (!options.allowExternalAudio)
1870
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1796
1871
  addDiagnostic(
1797
1872
  diagnostics,
1798
1873
  source,
@@ -1802,6 +1877,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1802
1877
  }
1803
1878
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1804
1879
  const name = token.name.toLowerCase();
1880
+ const tagStatus = featureStatusForTag(token.name, options);
1881
+ if (tagStatus === "preview")
1882
+ addDiagnostic(
1883
+ diagnostics,
1884
+ source,
1885
+ token.start,
1886
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
1887
+ "warning",
1888
+ "azure-preview-tag"
1889
+ );
1890
+ if (tagStatus === "deprecated")
1891
+ addDiagnostic(
1892
+ diagnostics,
1893
+ source,
1894
+ token.start,
1895
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
1896
+ "info",
1897
+ "azure-deprecated-tag"
1898
+ );
1805
1899
  if (name === "voice" && !attr(token, "name")?.trim())
1806
1900
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1807
1901
  if (name === "break") {
@@ -1922,28 +2016,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1922
2016
  validateAudioSource(token, source, diagnostics, options, "audio");
1923
2017
  }
1924
2018
  if (name === "mstts:turn") {
1925
- if (!attr(token, "voice")?.trim())
1926
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
2019
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
2020
+ addDiagnostic(
2021
+ diagnostics,
2022
+ source,
2023
+ token.start,
2024
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
2025
+ );
1927
2026
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1928
2027
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1929
2028
  }
1930
2029
  if (name === "mstts:backgroundaudio") {
1931
2030
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1932
2031
  const volume = attr(token, "volume");
1933
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
2032
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1934
2033
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1935
2034
  for (const [attribute, value] of [
1936
2035
  ["fadein", attr(token, "fadein")],
1937
2036
  ["fadeout", attr(token, "fadeout")]
1938
2037
  ]) {
1939
- if (value && !isValidAzureAudioDuration(value))
2038
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1940
2039
  addDiagnostic(
1941
2040
  diagnostics,
1942
2041
  source,
1943
2042
  token.start,
1944
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2043
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1945
2044
  );
1946
2045
  }
2046
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2047
+ addDiagnostic(
2048
+ diagnostics,
2049
+ source,
2050
+ token.start,
2051
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2052
+ );
1947
2053
  if (!token.selfClosing)
1948
2054
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1949
2055
  }
@@ -1975,6 +2081,16 @@ function validateAzureSsml(ssml, options = {}) {
1975
2081
  const tokens = tokenizeElements(ssml);
1976
2082
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1977
2083
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2084
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2085
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2086
+ if (index > 0)
2087
+ addDiagnostic(
2088
+ diagnostics,
2089
+ ssml,
2090
+ token.start,
2091
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2092
+ );
2093
+ }
1978
2094
  if (!speak || voices.length === 0)
1979
2095
  addDiagnostic(
1980
2096
  diagnostics,
@@ -1991,6 +2107,24 @@ function validateAzureSsml(ssml, options = {}) {
1991
2107
  const name = attr(token, "name")?.trim();
1992
2108
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1993
2109
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2110
+ if (name && definition?.status === "preview")
2111
+ addDiagnostic(
2112
+ diagnostics,
2113
+ ssml,
2114
+ token.start,
2115
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
2116
+ "warning",
2117
+ "azure-preview-voice"
2118
+ );
2119
+ if (name && definition?.status === "deprecated")
2120
+ addDiagnostic(
2121
+ diagnostics,
2122
+ ssml,
2123
+ token.start,
2124
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
2125
+ "info",
2126
+ "azure-deprecated-voice"
2127
+ );
1994
2128
  if (name && !definition && policySeverity)
1995
2129
  addDiagnostic(
1996
2130
  diagnostics,