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/index.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  validateAzureSsml,
15
15
  validateSsml,
16
16
  validateSsmlStructureIntegrity
17
- } from "./chunk-7LCSH4SZ.mjs";
17
+ } from "./chunk-GW6CKHXF.mjs";
18
18
  import {
19
19
  __privateAdd,
20
20
  __privateGet,
package/dist/react.d.mts CHANGED
@@ -113,6 +113,7 @@ interface SsmlDialogNode extends SsmlElementBase {
113
113
  interface SsmlTurnNode extends SsmlElementBase {
114
114
  type: "mstts:turn";
115
115
  voice?: string;
116
+ speaker?: string;
116
117
  }
117
118
  interface SsmlBackgroundAudioNode extends SsmlElementBase {
118
119
  type: "mstts:backgroundaudio";
@@ -126,12 +127,18 @@ interface SsmlBackgroundAudioNode extends SsmlElementBase {
126
127
  }
127
128
  interface MsttsTtsEmbeddingElement extends SsmlElementBase {
128
129
  type: "mstts:ttsembedding";
130
+ speakerProfileId?: string;
129
131
  }
130
132
  interface MsttsEmbeddingElement extends SsmlElementBase {
131
133
  type: "mstts:embedding";
134
+ id?: string;
135
+ speakerProfileId?: string;
132
136
  }
133
137
  interface MsttsVoiceConversionElement extends SsmlElementBase {
134
138
  type: "mstts:voiceconversion";
139
+ url?: string;
140
+ profile?: string;
141
+ speakerProfileId?: string;
135
142
  }
136
143
  interface CustomElement extends SsmlElementBase {
137
144
  type: "custom" | "element";
package/dist/react.d.ts CHANGED
@@ -113,6 +113,7 @@ interface SsmlDialogNode extends SsmlElementBase {
113
113
  interface SsmlTurnNode extends SsmlElementBase {
114
114
  type: "mstts:turn";
115
115
  voice?: string;
116
+ speaker?: string;
116
117
  }
117
118
  interface SsmlBackgroundAudioNode extends SsmlElementBase {
118
119
  type: "mstts:backgroundaudio";
@@ -126,12 +127,18 @@ interface SsmlBackgroundAudioNode extends SsmlElementBase {
126
127
  }
127
128
  interface MsttsTtsEmbeddingElement extends SsmlElementBase {
128
129
  type: "mstts:ttsembedding";
130
+ speakerProfileId?: string;
129
131
  }
130
132
  interface MsttsEmbeddingElement extends SsmlElementBase {
131
133
  type: "mstts:embedding";
134
+ id?: string;
135
+ speakerProfileId?: string;
132
136
  }
133
137
  interface MsttsVoiceConversionElement extends SsmlElementBase {
134
138
  type: "mstts:voiceconversion";
139
+ url?: string;
140
+ profile?: string;
141
+ speakerProfileId?: string;
135
142
  }
136
143
  interface CustomElement extends SsmlElementBase {
137
144
  type: "custom" | "element";
package/dist/react.js CHANGED
@@ -1724,6 +1724,7 @@ var SSML_ATTRS = {
1724
1724
  MSTTS_XMLNS: "xmlns:mstts",
1725
1725
  NAME: "name",
1726
1726
  VOICE: "voice",
1727
+ SPEAKER: "speaker",
1727
1728
  EFFECT: "effect",
1728
1729
  RATE: "rate",
1729
1730
  PITCH: "pitch",
@@ -1754,6 +1755,11 @@ var SSML_ATTRS = {
1754
1755
  ALIAS: "alias",
1755
1756
  MARK: "mark",
1756
1757
  URI: "uri",
1758
+ ID: "id",
1759
+ MODEL: "model",
1760
+ PROFILE: "profile",
1761
+ URL: "url",
1762
+ SPEAKER_PROFILE_ID: "speakerProfileId",
1757
1763
  TYPE: "type",
1758
1764
  VALUE: "value",
1759
1765
  FADE_IN: "fadein",
@@ -1849,6 +1855,7 @@ function getAttributes(element) {
1849
1855
  break;
1850
1856
  case SSML_TAGS.MSTTS_TURN:
1851
1857
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1858
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
1852
1859
  break;
1853
1860
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1854
1861
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -1857,9 +1864,18 @@ function getAttributes(element) {
1857
1864
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1858
1865
  break;
1859
1866
  case SSML_TAGS.MSTTS_DIALOG:
1867
+ break;
1860
1868
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1869
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1870
+ break;
1861
1871
  case SSML_TAGS.MSTTS_EMBEDDING:
1872
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
1873
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1874
+ break;
1862
1875
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1876
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
1877
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
1878
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1863
1879
  break;
1864
1880
  case SSML_TAGS.PARAGRAPH:
1865
1881
  case SSML_TAGS.SENTENCE:
@@ -2432,7 +2448,9 @@ function convertElement(node) {
2432
2448
  case SSML_TAGS.MSTTS_TURN: {
2433
2449
  const element = { type: SSML_TAGS.MSTTS_TURN };
2434
2450
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2451
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
2435
2452
  if (voice !== void 0) element.voice = voice;
2453
+ if (speaker !== void 0) element.speaker = speaker;
2436
2454
  return finishElement(element, node, attributes);
2437
2455
  }
2438
2456
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -2449,14 +2467,26 @@ function convertElement(node) {
2449
2467
  }
2450
2468
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2451
2469
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2470
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2471
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2452
2472
  return finishElement(element, node, attributes);
2453
2473
  }
2454
2474
  case SSML_TAGS.MSTTS_EMBEDDING: {
2455
2475
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2476
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
2477
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2478
+ if (id !== void 0) element.id = id;
2479
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2456
2480
  return finishElement(element, node, attributes);
2457
2481
  }
2458
2482
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2459
2483
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2484
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
2485
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
2486
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2487
+ if (url !== void 0) element.url = url;
2488
+ if (profile !== void 0) element.profile = profile;
2489
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2460
2490
  return finishElement(element, node, attributes);
2461
2491
  }
2462
2492
  default: {
@@ -2791,11 +2821,14 @@ function tokenizeElements(source) {
2791
2821
  }
2792
2822
  const selfClosing = /\/\s*>$/.test(raw);
2793
2823
  const parent = openElements[openElements.length - 1];
2824
+ const childElementIndex = parent?.childElementCount;
2825
+ if (parent) parent.childElementCount += 1;
2794
2826
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2795
2827
  const tokenName = nameMatch[1];
2796
2828
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2797
2829
  tokens.push({
2798
2830
  attributes,
2831
+ childElementIndex,
2799
2832
  end,
2800
2833
  name: tokenName,
2801
2834
  parentName: parent?.name,
@@ -2805,6 +2838,7 @@ function tokenizeElements(source) {
2805
2838
  });
2806
2839
  if (!selfClosing) {
2807
2840
  openElements.push({
2841
+ childElementCount: 0,
2808
2842
  name: tokenName,
2809
2843
  voiceName: tokenVoiceName
2810
2844
  });
@@ -2843,6 +2877,12 @@ function isValidAzureAudioDuration(value) {
2843
2877
  if (!clock) return false;
2844
2878
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2845
2879
  }
2880
+ function isValidAzureBackgroundAudioDuration(value) {
2881
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
2882
+ if (!match) return false;
2883
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
2884
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
2885
+ }
2846
2886
  function attr(token, name) {
2847
2887
  return token.attributes.get(name.toLowerCase());
2848
2888
  }
@@ -2972,9 +3012,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2972
3012
  }
2973
3013
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2974
3014
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2975
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
3015
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3016
+ try {
3017
+ return new URL(allowedOrigin).origin === parsed.origin;
3018
+ } catch {
3019
+ return allowedOrigin === parsed.origin;
3020
+ }
3021
+ }) ?? false;
3022
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
2976
3023
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2977
- else if (!options.allowExternalAudio)
3024
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
2978
3025
  addDiagnostic(
2979
3026
  diagnostics,
2980
3027
  source,
@@ -3104,28 +3151,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
3104
3151
  validateAudioSource(token, source, diagnostics, options, "audio");
3105
3152
  }
3106
3153
  if (name === "mstts:turn") {
3107
- if (!attr(token, "voice")?.trim())
3108
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
3154
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
3155
+ addDiagnostic(
3156
+ diagnostics,
3157
+ source,
3158
+ token.start,
3159
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
3160
+ );
3109
3161
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
3110
3162
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
3111
3163
  }
3112
3164
  if (name === "mstts:backgroundaudio") {
3113
3165
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
3114
3166
  const volume = attr(token, "volume");
3115
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
3167
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
3116
3168
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
3117
3169
  for (const [attribute, value] of [
3118
3170
  ["fadein", attr(token, "fadein")],
3119
3171
  ["fadeout", attr(token, "fadeout")]
3120
3172
  ]) {
3121
- if (value && !isValidAzureAudioDuration(value))
3173
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
3122
3174
  addDiagnostic(
3123
3175
  diagnostics,
3124
3176
  source,
3125
3177
  token.start,
3126
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
3178
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
3127
3179
  );
3128
3180
  }
3181
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
3182
+ addDiagnostic(
3183
+ diagnostics,
3184
+ source,
3185
+ token.start,
3186
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
3187
+ );
3129
3188
  if (!token.selfClosing)
3130
3189
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3131
3190
  }
@@ -3157,6 +3216,16 @@ function validateAzureSsml(ssml, options = {}) {
3157
3216
  const tokens = tokenizeElements(ssml);
3158
3217
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
3159
3218
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
3219
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
3220
+ for (const [index, token] of backgroundAudioTokens.entries()) {
3221
+ if (index > 0)
3222
+ addDiagnostic(
3223
+ diagnostics,
3224
+ ssml,
3225
+ token.start,
3226
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
3227
+ );
3228
+ }
3160
3229
  if (!speak || voices.length === 0)
3161
3230
  addDiagnostic(
3162
3231
  diagnostics,