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/core.d.ts CHANGED
@@ -109,6 +109,7 @@ interface SsmlDialogNode extends SsmlElementBase {
109
109
  interface SsmlTurnNode extends SsmlElementBase {
110
110
  type: "mstts:turn";
111
111
  voice?: string;
112
+ speaker?: string;
112
113
  }
113
114
  interface SsmlBackgroundAudioNode extends SsmlElementBase {
114
115
  type: "mstts:backgroundaudio";
@@ -122,12 +123,18 @@ interface SsmlBackgroundAudioNode extends SsmlElementBase {
122
123
  }
123
124
  interface MsttsTtsEmbeddingElement extends SsmlElementBase {
124
125
  type: "mstts:ttsembedding";
126
+ speakerProfileId?: string;
125
127
  }
126
128
  interface MsttsEmbeddingElement extends SsmlElementBase {
127
129
  type: "mstts:embedding";
130
+ id?: string;
131
+ speakerProfileId?: string;
128
132
  }
129
133
  interface MsttsVoiceConversionElement extends SsmlElementBase {
130
134
  type: "mstts:voiceconversion";
135
+ url?: string;
136
+ profile?: string;
137
+ speakerProfileId?: string;
131
138
  }
132
139
  type SsmlTtsEmbeddingNode = MsttsTtsEmbeddingElement;
133
140
  type SsmlEmbeddingNode = MsttsEmbeddingElement;
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
  }
@@ -1599,11 +1642,14 @@ function tokenizeElements(source) {
1599
1642
  }
1600
1643
  const selfClosing = /\/\s*>$/.test(raw);
1601
1644
  const parent = openElements[openElements.length - 1];
1645
+ const childElementIndex = parent?.childElementCount;
1646
+ if (parent) parent.childElementCount += 1;
1602
1647
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1603
1648
  const tokenName = nameMatch[1];
1604
1649
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1605
1650
  tokens.push({
1606
1651
  attributes,
1652
+ childElementIndex,
1607
1653
  end,
1608
1654
  name: tokenName,
1609
1655
  parentName: parent?.name,
@@ -1613,6 +1659,7 @@ function tokenizeElements(source) {
1613
1659
  });
1614
1660
  if (!selfClosing) {
1615
1661
  openElements.push({
1662
+ childElementCount: 0,
1616
1663
  name: tokenName,
1617
1664
  voiceName: tokenVoiceName
1618
1665
  });
@@ -1651,6 +1698,12 @@ function isValidAzureAudioDuration(value) {
1651
1698
  if (!clock) return false;
1652
1699
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1653
1700
  }
1701
+ function isValidAzureBackgroundAudioDuration(value) {
1702
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1703
+ if (!match) return false;
1704
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1705
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1706
+ }
1654
1707
  function attr(token, name) {
1655
1708
  return token.attributes.get(name.toLowerCase());
1656
1709
  }
@@ -1790,9 +1843,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1790
1843
  }
1791
1844
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1792
1845
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1793
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1846
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1847
+ try {
1848
+ return new URL(allowedOrigin).origin === parsed.origin;
1849
+ } catch {
1850
+ return allowedOrigin === parsed.origin;
1851
+ }
1852
+ }) ?? false;
1853
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1794
1854
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1795
- else if (!options.allowExternalAudio)
1855
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1796
1856
  addDiagnostic(
1797
1857
  diagnostics,
1798
1858
  source,
@@ -1922,28 +1982,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1922
1982
  validateAudioSource(token, source, diagnostics, options, "audio");
1923
1983
  }
1924
1984
  if (name === "mstts:turn") {
1925
- if (!attr(token, "voice")?.trim())
1926
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1985
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
1986
+ addDiagnostic(
1987
+ diagnostics,
1988
+ source,
1989
+ token.start,
1990
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
1991
+ );
1927
1992
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1928
1993
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1929
1994
  }
1930
1995
  if (name === "mstts:backgroundaudio") {
1931
1996
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1932
1997
  const volume = attr(token, "volume");
1933
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
1998
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1934
1999
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1935
2000
  for (const [attribute, value] of [
1936
2001
  ["fadein", attr(token, "fadein")],
1937
2002
  ["fadeout", attr(token, "fadeout")]
1938
2003
  ]) {
1939
- if (value && !isValidAzureAudioDuration(value))
2004
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1940
2005
  addDiagnostic(
1941
2006
  diagnostics,
1942
2007
  source,
1943
2008
  token.start,
1944
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2009
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1945
2010
  );
1946
2011
  }
2012
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2013
+ addDiagnostic(
2014
+ diagnostics,
2015
+ source,
2016
+ token.start,
2017
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2018
+ );
1947
2019
  if (!token.selfClosing)
1948
2020
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1949
2021
  }
@@ -1975,6 +2047,16 @@ function validateAzureSsml(ssml, options = {}) {
1975
2047
  const tokens = tokenizeElements(ssml);
1976
2048
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1977
2049
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2050
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2051
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2052
+ if (index > 0)
2053
+ addDiagnostic(
2054
+ diagnostics,
2055
+ ssml,
2056
+ token.start,
2057
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2058
+ );
2059
+ }
1978
2060
  if (!speak || voices.length === 0)
1979
2061
  addDiagnostic(
1980
2062
  diagnostics,