ssml-builder-js 2.10.0 → 2.12.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/react.js CHANGED
@@ -57,6 +57,7 @@ var ssmlPresets_exports = {};
57
57
  __export(ssmlPresets_exports, {
58
58
  AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
59
59
  AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
60
+ BACKGROUND_AUDIO_DURATION_PRESETS: () => BACKGROUND_AUDIO_DURATION_PRESETS,
60
61
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
61
62
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
62
63
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -138,6 +139,9 @@ var AZURE_VOICE_STYLE_MAP = {
138
139
  ],
139
140
  "es-ES-ElviraNeural": [],
140
141
  "fil-PH-AngeloNeural": [],
142
+ "fil-PH-Angelo:DragonHDLatestNeural": [],
143
+ "fil-PH-BlessicaNeural": [],
144
+ "fil-PH-Blessica:DragonHDLatestNeural": [],
141
145
  "fr-FR-DeniseNeural": ["cheerful", "sad"],
142
146
  "fr-FR-HenriNeural": ["cheerful", "sad"],
143
147
  "id-ID-GadisNeural": [],
@@ -415,6 +419,7 @@ var SAY_AS_PRESETS = [
415
419
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
416
420
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
417
421
  var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
422
+ var BACKGROUND_AUDIO_DURATION_PRESETS = ["0", "500", "1000", "3000", "10000"];
418
423
  var SILENCE_TYPE_PRESETS = [
419
424
  "Leading",
420
425
  "Tailing",
@@ -476,8 +481,8 @@ var SSML_ATTRIBUTE_PRESETS = {
476
481
  },
477
482
  "mstts:backgroundaudio": {
478
483
  volume: PROSODY_VOLUME_PRESETS,
479
- fadein: AUDIO_DURATION_PRESETS,
480
- fadeout: AUDIO_DURATION_PRESETS
484
+ fadein: BACKGROUND_AUDIO_DURATION_PRESETS,
485
+ fadeout: BACKGROUND_AUDIO_DURATION_PRESETS
481
486
  },
482
487
  silence: {
483
488
  type: SILENCE_TYPE_PRESETS,
@@ -2668,6 +2673,9 @@ var AZURE_VOICE_DEFINITIONS = [
2668
2673
  },
2669
2674
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
2670
2675
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
2676
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
2677
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
2678
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
2671
2679
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2672
2680
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2673
2681
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -2762,6 +2770,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
2762
2770
  "Enumerationcomma"
2763
2771
  ]);
2764
2772
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
2773
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
2774
+ function featureStatusForTag(name, options) {
2775
+ const tagName = canonicalTagName(name);
2776
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
2777
+ ([candidate]) => canonicalTagName(candidate) === tagName
2778
+ )?.[1];
2779
+ if (configured) return configured;
2780
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
2781
+ return "preview";
2782
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
2783
+ return void 0;
2784
+ }
2765
2785
  function decodeAttribute(value) {
2766
2786
  return value.replace(
2767
2787
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -2830,6 +2850,7 @@ function tokenizeElements(source) {
2830
2850
  attributes,
2831
2851
  childElementIndex,
2832
2852
  end,
2853
+ depth: openElements.length + 1,
2833
2854
  name: tokenName,
2834
2855
  parentName: parent?.name,
2835
2856
  parentVoiceName,
@@ -2878,9 +2899,9 @@ function isValidAzureAudioDuration(value) {
2878
2899
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2879
2900
  }
2880
2901
  function isValidAzureBackgroundAudioDuration(value) {
2881
- const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
2902
+ const match = /^(\d+)$/.exec(value.trim());
2882
2903
  if (!match) return false;
2883
- const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
2904
+ const milliseconds = Number(match[1]);
2884
2905
  return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
2885
2906
  }
2886
2907
  function attr(token, name) {
@@ -3010,13 +3031,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
3010
3031
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
3011
3032
  return;
3012
3033
  }
3034
+ if (parsed.username || parsed.password)
3035
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
3013
3036
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
3014
3037
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
3015
3038
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3016
3039
  try {
3017
- return new URL(allowedOrigin).origin === parsed.origin;
3040
+ const configured = new URL(allowedOrigin);
3041
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
3042
+ return false;
3043
+ return configured.origin === parsed.origin;
3018
3044
  } catch {
3019
- return allowedOrigin === parsed.origin;
3045
+ return false;
3020
3046
  }
3021
3047
  }) ?? false;
3022
3048
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -3031,6 +3057,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
3031
3057
  }
3032
3058
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
3033
3059
  const name = token.name.toLowerCase();
3060
+ const tagStatus = featureStatusForTag(token.name, options);
3061
+ if (tagStatus === "preview")
3062
+ addDiagnostic(
3063
+ diagnostics,
3064
+ source,
3065
+ token.start,
3066
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
3067
+ "warning",
3068
+ "azure-preview-tag"
3069
+ );
3070
+ if (tagStatus === "deprecated")
3071
+ addDiagnostic(
3072
+ diagnostics,
3073
+ source,
3074
+ token.start,
3075
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
3076
+ "info",
3077
+ "azure-deprecated-tag"
3078
+ );
3034
3079
  if (name === "voice" && !attr(token, "name")?.trim())
3035
3080
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
3036
3081
  if (name === "break") {
@@ -3205,6 +3250,9 @@ function validateAzureSsml(ssml, options = {}) {
3205
3250
  const maxLength = options.maxLength ?? 1e4;
3206
3251
  if (ssml.length > maxLength)
3207
3252
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
3253
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
3254
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
3255
+ }
3208
3256
  try {
3209
3257
  parseSsml(ssml);
3210
3258
  } catch (error) {
@@ -3214,6 +3262,18 @@ function validateAzureSsml(ssml, options = {}) {
3214
3262
  return diagnostics;
3215
3263
  }
3216
3264
  const tokens = tokenizeElements(ssml);
3265
+ if (options.maxXmlDepth !== void 0) {
3266
+ for (const token of tokens) {
3267
+ if (token.depth > options.maxXmlDepth) {
3268
+ addDiagnostic(
3269
+ diagnostics,
3270
+ ssml,
3271
+ token.start,
3272
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
3273
+ );
3274
+ }
3275
+ }
3276
+ }
3217
3277
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
3218
3278
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
3219
3279
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -3242,6 +3302,24 @@ function validateAzureSsml(ssml, options = {}) {
3242
3302
  const name = attr(token, "name")?.trim();
3243
3303
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
3244
3304
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
3305
+ if (name && definition?.status === "preview")
3306
+ addDiagnostic(
3307
+ diagnostics,
3308
+ ssml,
3309
+ token.start,
3310
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
3311
+ "warning",
3312
+ "azure-preview-voice"
3313
+ );
3314
+ if (name && definition?.status === "deprecated")
3315
+ addDiagnostic(
3316
+ diagnostics,
3317
+ ssml,
3318
+ token.start,
3319
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
3320
+ "info",
3321
+ "azure-deprecated-voice"
3322
+ );
3245
3323
  if (name && !definition && policySeverity)
3246
3324
  addDiagnostic(
3247
3325
  diagnostics,
@@ -4929,7 +5007,7 @@ var SSML_COMPLETION_SNIPPETS = [
4929
5007
  },
4930
5008
  {
4931
5009
  label: "mstts:backgroundaudio",
4932
- insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
5010
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:70}" fadein="\${3:1000}" fadeout="\${4:1000}" />`
4933
5011
  },
4934
5012
  {
4935
5013
  label: "mstts:ttsembedding",
@@ -5630,18 +5708,18 @@ var SSML_TAG_DEFINITIONS = [
5630
5708
  },
5631
5709
  {
5632
5710
  name: "volume",
5633
- description: "The background audio volume, for example `-3dB` or `medium`.",
5634
- example: "-3dB"
5711
+ description: "The background audio volume from 0 to 100.",
5712
+ example: "70"
5635
5713
  },
5636
5714
  {
5637
5715
  name: "fadein",
5638
- description: "The fade-in duration, for example `1s`.",
5639
- example: "1s"
5716
+ description: "The fade-in duration in milliseconds from 0 to 10000.",
5717
+ example: "1000"
5640
5718
  },
5641
5719
  {
5642
5720
  name: "fadeout",
5643
- description: "The fade-out duration, for example `500ms`.",
5644
- example: "500ms"
5721
+ description: "The fade-out duration in milliseconds from 0 to 10000.",
5722
+ example: "1000"
5645
5723
  }
5646
5724
  ]
5647
5725
  },
@@ -6295,6 +6373,175 @@ function updateNodesAtPath(nodes, path, update) {
6295
6373
  function updateTextAtPath(document2, path, value) {
6296
6374
  return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
6297
6375
  }
6376
+ function getNodeAtPath(nodes, path) {
6377
+ let current;
6378
+ let currentNodes = nodes;
6379
+ for (const index of path) {
6380
+ current = currentNodes[index];
6381
+ if (current === void 0) return void 0;
6382
+ if (!isElement(current)) {
6383
+ currentNodes = [];
6384
+ } else {
6385
+ currentNodes = current.children ?? [];
6386
+ }
6387
+ }
6388
+ return current;
6389
+ }
6390
+ function updateElementAtPath(document2, path, update) {
6391
+ return {
6392
+ ...document2,
6393
+ children: updateNodesAtPath(document2.children ?? [], path, (node) => isElement(node) ? update(node) : node)
6394
+ };
6395
+ }
6396
+ function updateElementText(element, value) {
6397
+ return { ...element, children: [value] };
6398
+ }
6399
+ function updateOptionalElementProperty(document2, path, property, value) {
6400
+ return updateElementAtPath(document2, path, (element) => {
6401
+ const next = { ...element };
6402
+ if (value.trim()) next[property] = value;
6403
+ else delete next[property];
6404
+ return next;
6405
+ });
6406
+ }
6407
+ var VISUAL_ELEMENT_FIELDS = {
6408
+ voice: [
6409
+ { key: "name", label: "Voice name" },
6410
+ { key: "effect", label: "Effect" }
6411
+ ],
6412
+ prosody: [
6413
+ { key: "rate", label: "Rate" },
6414
+ { key: "pitch", label: "Pitch" },
6415
+ { key: "volume", label: "Volume" },
6416
+ { key: "contour", label: "Contour" },
6417
+ { key: "range", label: "Range" }
6418
+ ],
6419
+ "express-as": [
6420
+ { key: "style", label: "Style" },
6421
+ { key: "styleDegree", label: "Style degree" },
6422
+ { key: "role", label: "Role" }
6423
+ ],
6424
+ expressAs: [
6425
+ { key: "style", label: "Style" },
6426
+ { key: "styleDegree", label: "Style degree" },
6427
+ { key: "role", label: "Role" }
6428
+ ],
6429
+ "mstts:express-as": [
6430
+ { key: "style", label: "Style" },
6431
+ { key: "styleDegree", label: "Style degree" },
6432
+ { key: "role", label: "Role" }
6433
+ ],
6434
+ "say-as": [
6435
+ { key: "interpretAs", label: "Interpret as" },
6436
+ { key: "format", label: "Format" },
6437
+ { key: "detail", label: "Detail" }
6438
+ ],
6439
+ sayAs: [
6440
+ { key: "interpretAs", label: "Interpret as" },
6441
+ { key: "format", label: "Format" },
6442
+ { key: "detail", label: "Detail" }
6443
+ ],
6444
+ phoneme: [
6445
+ { key: "alphabet", label: "Alphabet" },
6446
+ { key: "ph", label: "Pronunciation" }
6447
+ ],
6448
+ emphasis: [{ key: "level", label: "Level" }],
6449
+ audio: [
6450
+ { key: "src", label: "Source URL" },
6451
+ { key: "desc", label: "Description" },
6452
+ { key: "clipBegin", label: "Clip begin" },
6453
+ { key: "clipEnd", label: "Clip end" },
6454
+ { key: "speed", label: "Speed" },
6455
+ { key: "repeatCount", label: "Repeat count" },
6456
+ { key: "repeatDuration", label: "Repeat duration" },
6457
+ { key: "soundLevel", label: "Sound level" }
6458
+ ],
6459
+ mark: [{ key: "name", label: "Mark name" }],
6460
+ bookmark: [{ key: "mark", label: "Bookmark name" }],
6461
+ sub: [{ key: "alias", label: "Alias" }],
6462
+ lang: [{ key: "lang", label: "Language" }],
6463
+ lexicon: [{ key: "uri", label: "Lexicon URI" }],
6464
+ "mstts:silence": [
6465
+ { key: "typeValue", label: "Silence type" },
6466
+ { key: "value", label: "Value" }
6467
+ ],
6468
+ silence: [
6469
+ { key: "typeValue", label: "Silence type" },
6470
+ { key: "value", label: "Value" }
6471
+ ],
6472
+ "mstts:audioduration": [{ key: "value", label: "Duration" }],
6473
+ "mstts:ttsembedding": [{ key: "speakerProfileId", label: "Speaker profile ID" }],
6474
+ "mstts:embedding": [
6475
+ { key: "id", label: "Embedding ID" },
6476
+ { key: "speakerProfileId", label: "Speaker profile ID" }
6477
+ ],
6478
+ "mstts:voiceconversion": [
6479
+ { key: "url", label: "Source URL" },
6480
+ { key: "profile", label: "Profile" },
6481
+ { key: "speakerProfileId", label: "Speaker profile ID" }
6482
+ ]
6483
+ };
6484
+ function getElementFields(element) {
6485
+ return VISUAL_ELEMENT_FIELDS[elementLabel(element)] ?? [];
6486
+ }
6487
+ function VisualElementInspector({
6488
+ document: document2,
6489
+ element,
6490
+ path,
6491
+ readOnly,
6492
+ commit
6493
+ }) {
6494
+ const fields = getElementFields(element);
6495
+ return /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`), fields.length === 0 ? /* @__PURE__ */ React.createElement("p", null, "This element is preserved in the visual tree. Edit its attributes in Code mode.") : fields.map((field) => {
6496
+ const value = element[field.key];
6497
+ const inputValue = value === void 0 ? "" : String(value);
6498
+ const inputId = `ssml-visual-${field.key}`;
6499
+ return /* @__PURE__ */ React.createElement("label", { key: field.key, htmlFor: inputId }, field.label, field.multiline ? /* @__PURE__ */ React.createElement(
6500
+ "textarea",
6501
+ {
6502
+ id: inputId,
6503
+ value: inputValue,
6504
+ readOnly,
6505
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
6506
+ }
6507
+ ) : /* @__PURE__ */ React.createElement(
6508
+ "input",
6509
+ {
6510
+ id: inputId,
6511
+ value: inputValue,
6512
+ readOnly,
6513
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
6514
+ }
6515
+ ));
6516
+ }));
6517
+ }
6518
+ function getElementAncestors(nodes, path, ancestors = []) {
6519
+ const [index, ...rest] = path;
6520
+ const node = nodes[index];
6521
+ if (!node || !isElement(node)) return ancestors;
6522
+ if (rest.length === 0) return ancestors;
6523
+ return getElementAncestors(node.children ?? [], rest, [...ancestors, node]);
6524
+ }
6525
+ function buildVisualPreview(document2, leaf, selection) {
6526
+ const start = Math.max(0, Math.min(selection.start, leaf.value.length));
6527
+ const end = Math.max(start, Math.min(selection.end, leaf.value.length));
6528
+ const value = start === end ? leaf.value : leaf.value.slice(start, end);
6529
+ const ancestors = getElementAncestors(document2.children ?? [], leaf.path);
6530
+ const children = ancestors.reduceRight(
6531
+ (current, ancestor) => [{ ...ancestor, children: current }],
6532
+ [value]
6533
+ );
6534
+ return buildSsml({ ...document2, children });
6535
+ }
6536
+ function addDialogTurn(document2, path) {
6537
+ return updateElementAtPath(document2, path, (element) => ({
6538
+ ...element,
6539
+ children: [
6540
+ ...element.children ?? [],
6541
+ { type: "mstts:turn", voice: "en-US-JennyNeural", children: ["New dialog turn"] }
6542
+ ]
6543
+ }));
6544
+ }
6298
6545
  function wrapTextAtPath(document2, path, start, end, tag, attributes) {
6299
6546
  return {
6300
6547
  ...document2,
@@ -6346,6 +6593,7 @@ function VisualSsmlEditor({
6346
6593
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
6347
6594
  const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
6348
6595
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
6596
+ const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
6349
6597
  const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
6350
6598
  const commit = (nextDocument) => onChange?.(nextDocument);
6351
6599
  const applyWrapper = (tag, attributes) => {
@@ -6363,7 +6611,79 @@ function VisualSsmlEditor({
6363
6611
  selectedPath,
6364
6612
  onSelect: setSelectedPath
6365
6613
  }
6366
- )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6614
+ )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Dialog"), /* @__PURE__ */ React.createElement("p", null, "Add and edit speaker turns in this dialog."), /* @__PURE__ */ React.createElement(
6615
+ "button",
6616
+ {
6617
+ type: "button",
6618
+ disabled: readOnly,
6619
+ onClick: () => commit(addDialogTurn(document2, selectedPath ?? []))
6620
+ },
6621
+ "Add turn"
6622
+ )) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:turn" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Dialog turn"), /* @__PURE__ */ React.createElement("label", null, "Voice", /* @__PURE__ */ React.createElement(
6623
+ "input",
6624
+ {
6625
+ value: selectedElement.voice ?? "",
6626
+ readOnly,
6627
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", event.target.value))
6628
+ }
6629
+ )), /* @__PURE__ */ React.createElement("label", null, "Speaker", /* @__PURE__ */ React.createElement(
6630
+ "input",
6631
+ {
6632
+ value: selectedElement.speaker ?? "",
6633
+ readOnly,
6634
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "speaker", event.target.value))
6635
+ }
6636
+ )), /* @__PURE__ */ React.createElement("label", null, "Turn text", /* @__PURE__ */ React.createElement(
6637
+ "textarea",
6638
+ {
6639
+ value: selectedElement.children?.filter((child) => typeof child === "string").join("") ?? "",
6640
+ readOnly,
6641
+ onChange: (event) => commit(
6642
+ updateElementAtPath(
6643
+ document2,
6644
+ selectedPath ?? [],
6645
+ (element) => updateElementText(element, event.target.value)
6646
+ )
6647
+ )
6648
+ }
6649
+ ))) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:backgroundaudio" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Background audio"), /* @__PURE__ */ React.createElement("label", null, "Source URL", /* @__PURE__ */ React.createElement(
6650
+ "input",
6651
+ {
6652
+ value: selectedElement.src ?? "",
6653
+ readOnly,
6654
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "src", event.target.value))
6655
+ }
6656
+ )), /* @__PURE__ */ React.createElement("label", null, "Volume", /* @__PURE__ */ React.createElement(
6657
+ "input",
6658
+ {
6659
+ value: selectedElement.volume === void 0 ? "" : String(selectedElement.volume),
6660
+ readOnly,
6661
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "volume", event.target.value))
6662
+ }
6663
+ )), /* @__PURE__ */ React.createElement("label", null, "Fade in", /* @__PURE__ */ React.createElement(
6664
+ "input",
6665
+ {
6666
+ value: selectedElement.fadeIn === void 0 ? "" : String(selectedElement.fadeIn),
6667
+ readOnly,
6668
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeIn", event.target.value))
6669
+ }
6670
+ )), /* @__PURE__ */ React.createElement("label", null, "Fade out", /* @__PURE__ */ React.createElement(
6671
+ "input",
6672
+ {
6673
+ value: selectedElement.fadeOut === void 0 ? "" : String(selectedElement.fadeOut),
6674
+ readOnly,
6675
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeOut", event.target.value))
6676
+ }
6677
+ ))) : selectedElement && isElement(selectedElement) ? /* @__PURE__ */ React.createElement(
6678
+ VisualElementInspector,
6679
+ {
6680
+ document: document2,
6681
+ element: selectedElement,
6682
+ path: selectedPath ?? [],
6683
+ readOnly,
6684
+ commit
6685
+ }
6686
+ ) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6367
6687
  "textarea",
6368
6688
  {
6369
6689
  value: selectedLeaf.value,
@@ -6391,7 +6711,7 @@ function VisualSsmlEditor({
6391
6711
  "button",
6392
6712
  {
6393
6713
  type: "button",
6394
- onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
6714
+ onClick: () => onPreviewSelection(buildVisualPreview(document2, selectedLeaf, selection))
6395
6715
  },
6396
6716
  "Preview selection"
6397
6717
  ))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));