ssml-builder-js 2.9.0 → 2.10.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
@@ -36,7 +36,7 @@ import {
36
36
  resolveExpressAsStyles,
37
37
  updateEditableText,
38
38
  validateAzureSsml
39
- } from "./chunk-JNJVTEL6.mjs";
39
+ } from "./chunk-BYIZQL2W.mjs";
40
40
  import "./chunk-6S5ODO6A.mjs";
41
41
 
42
42
  // packages/ssml-editor-react/src/ssmlInsertions.ts
package/dist/index.js CHANGED
@@ -108,6 +108,7 @@ var SSML_ATTRS = {
108
108
  MSTTS_XMLNS: "xmlns:mstts",
109
109
  NAME: "name",
110
110
  VOICE: "voice",
111
+ SPEAKER: "speaker",
111
112
  EFFECT: "effect",
112
113
  RATE: "rate",
113
114
  PITCH: "pitch",
@@ -138,6 +139,11 @@ var SSML_ATTRS = {
138
139
  ALIAS: "alias",
139
140
  MARK: "mark",
140
141
  URI: "uri",
142
+ ID: "id",
143
+ MODEL: "model",
144
+ PROFILE: "profile",
145
+ URL: "url",
146
+ SPEAKER_PROFILE_ID: "speakerProfileId",
141
147
  TYPE: "type",
142
148
  VALUE: "value",
143
149
  FADE_IN: "fadein",
@@ -235,6 +241,7 @@ function getAttributes(element) {
235
241
  break;
236
242
  case SSML_TAGS.MSTTS_TURN:
237
243
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
244
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
238
245
  break;
239
246
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
240
247
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -243,9 +250,18 @@ function getAttributes(element) {
243
250
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
244
251
  break;
245
252
  case SSML_TAGS.MSTTS_DIALOG:
253
+ break;
246
254
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
255
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
256
+ break;
247
257
  case SSML_TAGS.MSTTS_EMBEDDING:
258
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
259
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
260
+ break;
248
261
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
262
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
263
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
264
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
249
265
  break;
250
266
  case SSML_TAGS.PARAGRAPH:
251
267
  case SSML_TAGS.SENTENCE:
@@ -820,7 +836,9 @@ function convertElement(node) {
820
836
  case SSML_TAGS.MSTTS_TURN: {
821
837
  const element = { type: SSML_TAGS.MSTTS_TURN };
822
838
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
839
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
823
840
  if (voice !== void 0) element.voice = voice;
841
+ if (speaker !== void 0) element.speaker = speaker;
824
842
  return finishElement(element, node, attributes);
825
843
  }
826
844
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -837,14 +855,26 @@ function convertElement(node) {
837
855
  }
838
856
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
839
857
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
858
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
859
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
840
860
  return finishElement(element, node, attributes);
841
861
  }
842
862
  case SSML_TAGS.MSTTS_EMBEDDING: {
843
863
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
864
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
865
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
866
+ if (id !== void 0) element.id = id;
867
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
844
868
  return finishElement(element, node, attributes);
845
869
  }
846
870
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
847
871
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
872
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
873
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
874
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
875
+ if (url !== void 0) element.url = url;
876
+ if (profile !== void 0) element.profile = profile;
877
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
848
878
  return finishElement(element, node, attributes);
849
879
  }
850
880
  default: {
@@ -1220,6 +1250,7 @@ function elementAttributes(element) {
1220
1250
  break;
1221
1251
  case "mstts:turn":
1222
1252
  addAttribute2(attributes, "voice", element.voice);
1253
+ addAttribute2(attributes, "speaker", element.speaker);
1223
1254
  break;
1224
1255
  case "mstts:backgroundaudio":
1225
1256
  addAttribute2(attributes, "src", element.src);
@@ -1227,6 +1258,18 @@ function elementAttributes(element) {
1227
1258
  addAttribute2(attributes, "fadein", element.fadeIn ?? element.fadein);
1228
1259
  addAttribute2(attributes, "fadeout", element.fadeOut ?? element.fadeout);
1229
1260
  break;
1261
+ case "mstts:ttsembedding":
1262
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1263
+ break;
1264
+ case "mstts:embedding":
1265
+ addAttribute2(attributes, "id", element.id);
1266
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1267
+ break;
1268
+ case "mstts:voiceconversion":
1269
+ addAttribute2(attributes, "url", element.url);
1270
+ addAttribute2(attributes, "profile", element.profile);
1271
+ addAttribute2(attributes, "speakerProfileId", element.speakerProfileId);
1272
+ break;
1230
1273
  }
1231
1274
  return Object.fromEntries(Object.entries(attributes).map(([name, value]) => [name, String(value)]));
1232
1275
  }
@@ -1613,11 +1656,14 @@ function tokenizeElements(source) {
1613
1656
  }
1614
1657
  const selfClosing = /\/\s*>$/.test(raw);
1615
1658
  const parent = openElements[openElements.length - 1];
1659
+ const childElementIndex = parent?.childElementCount;
1660
+ if (parent) parent.childElementCount += 1;
1616
1661
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1617
1662
  const tokenName = nameMatch[1];
1618
1663
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1619
1664
  tokens.push({
1620
1665
  attributes,
1666
+ childElementIndex,
1621
1667
  end,
1622
1668
  name: tokenName,
1623
1669
  parentName: parent?.name,
@@ -1627,6 +1673,7 @@ function tokenizeElements(source) {
1627
1673
  });
1628
1674
  if (!selfClosing) {
1629
1675
  openElements.push({
1676
+ childElementCount: 0,
1630
1677
  name: tokenName,
1631
1678
  voiceName: tokenVoiceName
1632
1679
  });
@@ -1665,6 +1712,12 @@ function isValidAzureAudioDuration(value) {
1665
1712
  if (!clock) return false;
1666
1713
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1667
1714
  }
1715
+ function isValidAzureBackgroundAudioDuration(value) {
1716
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1717
+ if (!match) return false;
1718
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1719
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1720
+ }
1668
1721
  function attr(token, name) {
1669
1722
  return token.attributes.get(name.toLowerCase());
1670
1723
  }
@@ -1804,9 +1857,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1804
1857
  }
1805
1858
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1806
1859
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1807
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1860
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1861
+ try {
1862
+ return new URL(allowedOrigin).origin === parsed.origin;
1863
+ } catch {
1864
+ return allowedOrigin === parsed.origin;
1865
+ }
1866
+ }) ?? false;
1867
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1808
1868
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1809
- else if (!options.allowExternalAudio)
1869
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1810
1870
  addDiagnostic(
1811
1871
  diagnostics,
1812
1872
  source,
@@ -1936,28 +1996,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1936
1996
  validateAudioSource(token, source, diagnostics, options, "audio");
1937
1997
  }
1938
1998
  if (name === "mstts:turn") {
1939
- if (!attr(token, "voice")?.trim())
1940
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1999
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
2000
+ addDiagnostic(
2001
+ diagnostics,
2002
+ source,
2003
+ token.start,
2004
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
2005
+ );
1941
2006
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1942
2007
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1943
2008
  }
1944
2009
  if (name === "mstts:backgroundaudio") {
1945
2010
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1946
2011
  const volume = attr(token, "volume");
1947
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
2012
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1948
2013
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1949
2014
  for (const [attribute, value] of [
1950
2015
  ["fadein", attr(token, "fadein")],
1951
2016
  ["fadeout", attr(token, "fadeout")]
1952
2017
  ]) {
1953
- if (value && !isValidAzureAudioDuration(value))
2018
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1954
2019
  addDiagnostic(
1955
2020
  diagnostics,
1956
2021
  source,
1957
2022
  token.start,
1958
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2023
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1959
2024
  );
1960
2025
  }
2026
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2027
+ addDiagnostic(
2028
+ diagnostics,
2029
+ source,
2030
+ token.start,
2031
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2032
+ );
1961
2033
  if (!token.selfClosing)
1962
2034
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1963
2035
  }
@@ -1989,6 +2061,16 @@ function validateAzureSsml(ssml, options = {}) {
1989
2061
  const tokens = tokenizeElements(ssml);
1990
2062
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1991
2063
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2064
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2065
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2066
+ if (index > 0)
2067
+ addDiagnostic(
2068
+ diagnostics,
2069
+ ssml,
2070
+ token.start,
2071
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2072
+ );
2073
+ }
1992
2074
  if (!speak || voices.length === 0)
1993
2075
  addDiagnostic(
1994
2076
  diagnostics,