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.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  validateAzureSsml,
15
15
  validateSsml,
16
16
  validateSsmlStructureIntegrity
17
- } from "./chunk-7LCSH4SZ.mjs";
17
+ } from "./chunk-SNRI43QJ.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: {
@@ -926,6 +956,9 @@ var AZURE_VOICE_DEFINITIONS = [
926
956
  },
927
957
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
928
958
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
959
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
960
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
961
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
929
962
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
930
963
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
931
964
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -1020,6 +1053,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1020
1053
  "Enumerationcomma"
1021
1054
  ]);
1022
1055
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1056
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
1057
+ function featureStatusForTag(name, options) {
1058
+ const tagName = canonicalTagName(name);
1059
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
1060
+ ([candidate]) => canonicalTagName(candidate) === tagName
1061
+ )?.[1];
1062
+ if (configured) return configured;
1063
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
1064
+ return "preview";
1065
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
1066
+ return void 0;
1067
+ }
1023
1068
  function decodeAttribute(value) {
1024
1069
  return value.replace(
1025
1070
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -1079,11 +1124,14 @@ function tokenizeElements(source) {
1079
1124
  }
1080
1125
  const selfClosing = /\/\s*>$/.test(raw);
1081
1126
  const parent = openElements[openElements.length - 1];
1127
+ const childElementIndex = parent?.childElementCount;
1128
+ if (parent) parent.childElementCount += 1;
1082
1129
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
1083
1130
  const tokenName = nameMatch[1];
1084
1131
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
1085
1132
  tokens.push({
1086
1133
  attributes,
1134
+ childElementIndex,
1087
1135
  end,
1088
1136
  name: tokenName,
1089
1137
  parentName: parent?.name,
@@ -1093,6 +1141,7 @@ function tokenizeElements(source) {
1093
1141
  });
1094
1142
  if (!selfClosing) {
1095
1143
  openElements.push({
1144
+ childElementCount: 0,
1096
1145
  name: tokenName,
1097
1146
  voiceName: tokenVoiceName
1098
1147
  });
@@ -1131,6 +1180,12 @@ function isValidAzureAudioDuration(value) {
1131
1180
  if (!clock) return false;
1132
1181
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1133
1182
  }
1183
+ function isValidAzureBackgroundAudioDuration(value) {
1184
+ const match = /^(\d+)$/.exec(value.trim());
1185
+ if (!match) return false;
1186
+ const milliseconds = Number(match[1]);
1187
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1188
+ }
1134
1189
  function attr(token, name) {
1135
1190
  return token.attributes.get(name.toLowerCase());
1136
1191
  }
@@ -1260,9 +1315,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1260
1315
  }
1261
1316
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1262
1317
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1263
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
1318
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1319
+ try {
1320
+ return new URL(allowedOrigin).origin === parsed.origin;
1321
+ } catch {
1322
+ return allowedOrigin === parsed.origin;
1323
+ }
1324
+ }) ?? false;
1325
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
1264
1326
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
1265
- else if (!options.allowExternalAudio)
1327
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
1266
1328
  addDiagnostic(
1267
1329
  diagnostics,
1268
1330
  source,
@@ -1272,6 +1334,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1272
1334
  }
1273
1335
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1274
1336
  const name = token.name.toLowerCase();
1337
+ const tagStatus = featureStatusForTag(token.name, options);
1338
+ if (tagStatus === "preview")
1339
+ addDiagnostic(
1340
+ diagnostics,
1341
+ source,
1342
+ token.start,
1343
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
1344
+ "warning",
1345
+ "azure-preview-tag"
1346
+ );
1347
+ if (tagStatus === "deprecated")
1348
+ addDiagnostic(
1349
+ diagnostics,
1350
+ source,
1351
+ token.start,
1352
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
1353
+ "info",
1354
+ "azure-deprecated-tag"
1355
+ );
1275
1356
  if (name === "voice" && !attr(token, "name")?.trim())
1276
1357
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1277
1358
  if (name === "break") {
@@ -1392,28 +1473,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
1392
1473
  validateAudioSource(token, source, diagnostics, options, "audio");
1393
1474
  }
1394
1475
  if (name === "mstts:turn") {
1395
- if (!attr(token, "voice")?.trim())
1396
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
1476
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
1477
+ addDiagnostic(
1478
+ diagnostics,
1479
+ source,
1480
+ token.start,
1481
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
1482
+ );
1397
1483
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
1398
1484
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
1399
1485
  }
1400
1486
  if (name === "mstts:backgroundaudio") {
1401
1487
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
1402
1488
  const volume = attr(token, "volume");
1403
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
1489
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
1404
1490
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
1405
1491
  for (const [attribute, value] of [
1406
1492
  ["fadein", attr(token, "fadein")],
1407
1493
  ["fadeout", attr(token, "fadeout")]
1408
1494
  ]) {
1409
- if (value && !isValidAzureAudioDuration(value))
1495
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
1410
1496
  addDiagnostic(
1411
1497
  diagnostics,
1412
1498
  source,
1413
1499
  token.start,
1414
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
1500
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
1415
1501
  );
1416
1502
  }
1503
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
1504
+ addDiagnostic(
1505
+ diagnostics,
1506
+ source,
1507
+ token.start,
1508
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
1509
+ );
1417
1510
  if (!token.selfClosing)
1418
1511
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
1419
1512
  }
@@ -1445,6 +1538,16 @@ function validateAzureSsml(ssml, options = {}) {
1445
1538
  const tokens = tokenizeElements(ssml);
1446
1539
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
1447
1540
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
1541
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
1542
+ for (const [index, token] of backgroundAudioTokens.entries()) {
1543
+ if (index > 0)
1544
+ addDiagnostic(
1545
+ diagnostics,
1546
+ ssml,
1547
+ token.start,
1548
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
1549
+ );
1550
+ }
1448
1551
  if (!speak || voices.length === 0)
1449
1552
  addDiagnostic(
1450
1553
  diagnostics,
@@ -1461,6 +1564,24 @@ function validateAzureSsml(ssml, options = {}) {
1461
1564
  const name = attr(token, "name")?.trim();
1462
1565
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
1463
1566
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
1567
+ if (name && definition?.status === "preview")
1568
+ addDiagnostic(
1569
+ diagnostics,
1570
+ ssml,
1571
+ token.start,
1572
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
1573
+ "warning",
1574
+ "azure-preview-voice"
1575
+ );
1576
+ if (name && definition?.status === "deprecated")
1577
+ addDiagnostic(
1578
+ diagnostics,
1579
+ ssml,
1580
+ token.start,
1581
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
1582
+ "info",
1583
+ "azure-deprecated-voice"
1584
+ );
1464
1585
  if (name && !definition && policySeverity)
1465
1586
  addDiagnostic(
1466
1587
  diagnostics,
@@ -2226,6 +2347,7 @@ var ssmlPresets_exports = {};
2226
2347
  __export(ssmlPresets_exports, {
2227
2348
  AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
2228
2349
  AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
2350
+ BACKGROUND_AUDIO_DURATION_PRESETS: () => BACKGROUND_AUDIO_DURATION_PRESETS,
2229
2351
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
2230
2352
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
2231
2353
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -2307,6 +2429,9 @@ var AZURE_VOICE_STYLE_MAP = {
2307
2429
  ],
2308
2430
  "es-ES-ElviraNeural": [],
2309
2431
  "fil-PH-AngeloNeural": [],
2432
+ "fil-PH-Angelo:DragonHDLatestNeural": [],
2433
+ "fil-PH-BlessicaNeural": [],
2434
+ "fil-PH-Blessica:DragonHDLatestNeural": [],
2310
2435
  "fr-FR-DeniseNeural": ["cheerful", "sad"],
2311
2436
  "fr-FR-HenriNeural": ["cheerful", "sad"],
2312
2437
  "id-ID-GadisNeural": [],
@@ -2584,6 +2709,7 @@ var SAY_AS_PRESETS = [
2584
2709
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
2585
2710
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
2586
2711
  var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
2712
+ var BACKGROUND_AUDIO_DURATION_PRESETS = ["0", "500", "1000", "3000", "10000"];
2587
2713
  var SILENCE_TYPE_PRESETS = [
2588
2714
  "Leading",
2589
2715
  "Tailing",
@@ -2645,8 +2771,8 @@ var SSML_ATTRIBUTE_PRESETS = {
2645
2771
  },
2646
2772
  "mstts:backgroundaudio": {
2647
2773
  volume: PROSODY_VOLUME_PRESETS,
2648
- fadein: AUDIO_DURATION_PRESETS,
2649
- fadeout: AUDIO_DURATION_PRESETS
2774
+ fadein: BACKGROUND_AUDIO_DURATION_PRESETS,
2775
+ fadeout: BACKGROUND_AUDIO_DURATION_PRESETS
2650
2776
  },
2651
2777
  silence: {
2652
2778
  type: SILENCE_TYPE_PRESETS,
@@ -3151,7 +3277,7 @@ var SSML_COMPLETION_SNIPPETS = [
3151
3277
  },
3152
3278
  {
3153
3279
  label: "mstts:backgroundaudio",
3154
- insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
3280
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:70}" fadein="\${3:1000}" fadeout="\${4:1000}" />`
3155
3281
  },
3156
3282
  {
3157
3283
  label: "mstts:ttsembedding",
@@ -3930,18 +4056,18 @@ var SSML_TAG_DEFINITIONS = [
3930
4056
  },
3931
4057
  {
3932
4058
  name: "volume",
3933
- description: "The background audio volume, for example `-3dB` or `medium`.",
3934
- example: "-3dB"
4059
+ description: "The background audio volume from 0 to 100.",
4060
+ example: "70"
3935
4061
  },
3936
4062
  {
3937
4063
  name: "fadein",
3938
- description: "The fade-in duration, for example `1s`.",
3939
- example: "1s"
4064
+ description: "The fade-in duration in milliseconds from 0 to 10000.",
4065
+ example: "1000"
3940
4066
  },
3941
4067
  {
3942
4068
  name: "fadeout",
3943
- description: "The fade-out duration, for example `500ms`.",
3944
- example: "500ms"
4069
+ description: "The fade-out duration in milliseconds from 0 to 10000.",
4070
+ example: "1000"
3945
4071
  }
3946
4072
  ]
3947
4073
  },