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.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 "./chunk-6S5ODO6A.mjs";
19
19
  export {
20
20
  areAzureLanguagesEquivalent,
package/dist/elements.js CHANGED
@@ -91,6 +91,7 @@ var SSML_ATTRS = {
91
91
  MSTTS_XMLNS: "xmlns:mstts",
92
92
  NAME: "name",
93
93
  VOICE: "voice",
94
+ SPEAKER: "speaker",
94
95
  EFFECT: "effect",
95
96
  RATE: "rate",
96
97
  PITCH: "pitch",
@@ -121,6 +122,11 @@ var SSML_ATTRS = {
121
122
  ALIAS: "alias",
122
123
  MARK: "mark",
123
124
  URI: "uri",
125
+ ID: "id",
126
+ MODEL: "model",
127
+ PROFILE: "profile",
128
+ URL: "url",
129
+ SPEAKER_PROFILE_ID: "speakerProfileId",
124
130
  TYPE: "type",
125
131
  VALUE: "value",
126
132
  FADE_IN: "fadein",
@@ -216,6 +222,7 @@ function getAttributes(element) {
216
222
  break;
217
223
  case SSML_TAGS.MSTTS_TURN:
218
224
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
225
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
219
226
  break;
220
227
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
221
228
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -224,9 +231,18 @@ function getAttributes(element) {
224
231
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
225
232
  break;
226
233
  case SSML_TAGS.MSTTS_DIALOG:
234
+ break;
227
235
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
236
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
237
+ break;
228
238
  case SSML_TAGS.MSTTS_EMBEDDING:
239
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
240
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
241
+ break;
229
242
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
243
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
244
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
245
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
230
246
  break;
231
247
  case SSML_TAGS.PARAGRAPH:
232
248
  case SSML_TAGS.SENTENCE:
@@ -799,7 +815,9 @@ function convertElement(node) {
799
815
  case SSML_TAGS.MSTTS_TURN: {
800
816
  const element = { type: SSML_TAGS.MSTTS_TURN };
801
817
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
818
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
802
819
  if (voice !== void 0) element.voice = voice;
820
+ if (speaker !== void 0) element.speaker = speaker;
803
821
  return finishElement(element, node, attributes);
804
822
  }
805
823
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -816,14 +834,26 @@ function convertElement(node) {
816
834
  }
817
835
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
818
836
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
837
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
838
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
819
839
  return finishElement(element, node, attributes);
820
840
  }
821
841
  case SSML_TAGS.MSTTS_EMBEDDING: {
822
842
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
843
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
844
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
845
+ if (id !== void 0) element.id = id;
846
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
823
847
  return finishElement(element, node, attributes);
824
848
  }
825
849
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
826
850
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
851
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
852
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
853
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
854
+ if (url !== void 0) element.url = url;
855
+ if (profile !== void 0) element.profile = profile;
856
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
827
857
  return finishElement(element, node, attributes);
828
858
  }
829
859
  default: {
@@ -1079,11 +1109,14 @@ function tokenizeElements(source) {
1079
1109
  }
1080
1110
  const selfClosing = /\/\s*>$/.test(raw);
1081
1111
  const parent = openElements[openElements.length - 1];
1112
+ const childElementIndex = parent?.childElementCount;
1113
+ if (parent) parent.childElementCount += 1;
1082
1114
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1083
1115
  const tokenName = nameMatch[1];
1084
1116
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1085
1117
  tokens.push({
1086
1118
  attributes,
1119
+ childElementIndex,
1087
1120
  end,
1088
1121
  name: tokenName,
1089
1122
  parentName: parent?.name,
@@ -1093,6 +1126,7 @@ function tokenizeElements(source) {
1093
1126
  });
1094
1127
  if (!selfClosing) {
1095
1128
  openElements.push({
1129
+ childElementCount: 0,
1096
1130
  name: tokenName,
1097
1131
  voiceName: tokenVoiceName
1098
1132
  });
@@ -1131,6 +1165,12 @@ function isValidAzureAudioDuration(value) {
1131
1165
  if (!clock) return false;
1132
1166
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1133
1167
  }
1168
+ function isValidAzureBackgroundAudioDuration(value) {
1169
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1170
+ if (!match) return false;
1171
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1172
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1173
+ }
1134
1174
  function attr(token, name) {
1135
1175
  return token.attributes.get(name.toLowerCase());
1136
1176
  }
@@ -1260,9 +1300,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1260
1300
  }
1261
1301
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1262
1302
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1263
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1303
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1304
+ try {
1305
+ return new URL(allowedOrigin).origin === parsed.origin;
1306
+ } catch {
1307
+ return allowedOrigin === parsed.origin;
1308
+ }
1309
+ }) ?? false;
1310
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1264
1311
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1265
- else if (!options.allowExternalAudio)
1312
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1266
1313
  addDiagnostic(
1267
1314
  diagnostics,
1268
1315
  source,
@@ -1392,28 +1439,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1392
1439
  validateAudioSource(token, source, diagnostics, options, "audio");
1393
1440
  }
1394
1441
  if (name === "mstts:turn") {
1395
- if (!attr(token, "voice")?.trim())
1396
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1442
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
1443
+ addDiagnostic(
1444
+ diagnostics,
1445
+ source,
1446
+ token.start,
1447
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
1448
+ );
1397
1449
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1398
1450
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1399
1451
  }
1400
1452
  if (name === "mstts:backgroundaudio") {
1401
1453
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1402
1454
  const volume = attr(token, "volume");
1403
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
1455
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1404
1456
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1405
1457
  for (const [attribute, value] of [
1406
1458
  ["fadein", attr(token, "fadein")],
1407
1459
  ["fadeout", attr(token, "fadeout")]
1408
1460
  ]) {
1409
- if (value && !isValidAzureAudioDuration(value))
1461
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1410
1462
  addDiagnostic(
1411
1463
  diagnostics,
1412
1464
  source,
1413
1465
  token.start,
1414
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
1466
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1415
1467
  );
1416
1468
  }
1469
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
1470
+ addDiagnostic(
1471
+ diagnostics,
1472
+ source,
1473
+ token.start,
1474
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
1475
+ );
1417
1476
  if (!token.selfClosing)
1418
1477
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1419
1478
  }
@@ -1445,6 +1504,16 @@ function validateAzureSsml(ssml, options = {}) {
1445
1504
  const tokens = tokenizeElements(ssml);
1446
1505
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1447
1506
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
1507
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
1508
+ for (const [index, token] of backgroundAudioTokens.entries()) {
1509
+ if (index > 0)
1510
+ addDiagnostic(
1511
+ diagnostics,
1512
+ ssml,
1513
+ token.start,
1514
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
1515
+ );
1516
+ }
1448
1517
  if (!speak || voices.length === 0)
1449
1518
  addDiagnostic(
1450
1519
  diagnostics,