@ssml-builder-js/ssml-editor-react 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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @ssml-builder-js/ssml-editor-react
2
2
 
3
+ ## 2.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add Visual Editor forms for the supported SSML and Azure extension elements and preserve parent context during selection preview.
8
+
9
+ ## 2.11.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Release v2.11.0 with Azure voice catalog APIs and synchronization tools, expanded editor voice support, and stricter background audio validation.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @ssml-builder-js/ssml-core@2.11.0
19
+
3
20
  ## 2.10.0
4
21
 
5
22
  ### Minor Changes
package/dist/index.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,
@@ -3326,7 +3331,7 @@ var SSML_COMPLETION_SNIPPETS = [
3326
3331
  },
3327
3332
  {
3328
3333
  label: "mstts:backgroundaudio",
3329
- insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
3334
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:70}" fadein="\${3:1000}" fadeout="\${4:1000}" />`
3330
3335
  },
3331
3336
  {
3332
3337
  label: "mstts:ttsembedding",
@@ -4027,18 +4032,18 @@ var SSML_TAG_DEFINITIONS = [
4027
4032
  },
4028
4033
  {
4029
4034
  name: "volume",
4030
- description: "The background audio volume, for example `-3dB` or `medium`.",
4031
- example: "-3dB"
4035
+ description: "The background audio volume from 0 to 100.",
4036
+ example: "70"
4032
4037
  },
4033
4038
  {
4034
4039
  name: "fadein",
4035
- description: "The fade-in duration, for example `1s`.",
4036
- example: "1s"
4040
+ description: "The fade-in duration in milliseconds from 0 to 10000.",
4041
+ example: "1000"
4037
4042
  },
4038
4043
  {
4039
4044
  name: "fadeout",
4040
- description: "The fade-out duration, for example `500ms`.",
4041
- example: "500ms"
4045
+ description: "The fade-out duration in milliseconds from 0 to 10000.",
4046
+ example: "1000"
4042
4047
  }
4043
4048
  ]
4044
4049
  },
@@ -4694,6 +4699,181 @@ function updateNodesAtPath(nodes, path, update) {
4694
4699
  function updateTextAtPath(document2, path, value) {
4695
4700
  return { ...document2, children: updateNodesAtPath(document2.children ?? [], path, () => value) };
4696
4701
  }
4702
+ function getNodeAtPath(nodes, path) {
4703
+ let current;
4704
+ let currentNodes = nodes;
4705
+ for (const index of path) {
4706
+ current = currentNodes[index];
4707
+ if (current === void 0) return void 0;
4708
+ if (!isElement(current)) {
4709
+ currentNodes = [];
4710
+ } else {
4711
+ currentNodes = current.children ?? [];
4712
+ }
4713
+ }
4714
+ return current;
4715
+ }
4716
+ function updateElementAtPath(document2, path, update) {
4717
+ return {
4718
+ ...document2,
4719
+ children: updateNodesAtPath(document2.children ?? [], path, (node) => isElement(node) ? update(node) : node)
4720
+ };
4721
+ }
4722
+ function updateElementText(element, value) {
4723
+ return { ...element, children: [value] };
4724
+ }
4725
+ function updateOptionalElementProperty(document2, path, property, value) {
4726
+ return updateElementAtPath(document2, path, (element) => {
4727
+ const next = { ...element };
4728
+ if (value.trim()) next[property] = value;
4729
+ else delete next[property];
4730
+ return next;
4731
+ });
4732
+ }
4733
+ var VISUAL_ELEMENT_FIELDS = {
4734
+ voice: [
4735
+ { key: "name", label: "Voice name" },
4736
+ { key: "effect", label: "Effect" }
4737
+ ],
4738
+ prosody: [
4739
+ { key: "rate", label: "Rate" },
4740
+ { key: "pitch", label: "Pitch" },
4741
+ { key: "volume", label: "Volume" },
4742
+ { key: "contour", label: "Contour" },
4743
+ { key: "range", label: "Range" }
4744
+ ],
4745
+ "express-as": [
4746
+ { key: "style", label: "Style" },
4747
+ { key: "styleDegree", label: "Style degree" },
4748
+ { key: "role", label: "Role" }
4749
+ ],
4750
+ expressAs: [
4751
+ { key: "style", label: "Style" },
4752
+ { key: "styleDegree", label: "Style degree" },
4753
+ { key: "role", label: "Role" }
4754
+ ],
4755
+ "mstts:express-as": [
4756
+ { key: "style", label: "Style" },
4757
+ { key: "styleDegree", label: "Style degree" },
4758
+ { key: "role", label: "Role" }
4759
+ ],
4760
+ "say-as": [
4761
+ { key: "interpretAs", label: "Interpret as" },
4762
+ { key: "format", label: "Format" },
4763
+ { key: "detail", label: "Detail" }
4764
+ ],
4765
+ sayAs: [
4766
+ { key: "interpretAs", label: "Interpret as" },
4767
+ { key: "format", label: "Format" },
4768
+ { key: "detail", label: "Detail" }
4769
+ ],
4770
+ phoneme: [
4771
+ { key: "alphabet", label: "Alphabet" },
4772
+ { key: "ph", label: "Pronunciation" }
4773
+ ],
4774
+ emphasis: [{ key: "level", label: "Level" }],
4775
+ audio: [
4776
+ { key: "src", label: "Source URL" },
4777
+ { key: "desc", label: "Description" },
4778
+ { key: "clipBegin", label: "Clip begin" },
4779
+ { key: "clipEnd", label: "Clip end" },
4780
+ { key: "speed", label: "Speed" },
4781
+ { key: "repeatCount", label: "Repeat count" },
4782
+ { key: "repeatDuration", label: "Repeat duration" },
4783
+ { key: "soundLevel", label: "Sound level" }
4784
+ ],
4785
+ mark: [{ key: "name", label: "Mark name" }],
4786
+ bookmark: [{ key: "mark", label: "Bookmark name" }],
4787
+ sub: [{ key: "alias", label: "Alias" }],
4788
+ lang: [{ key: "lang", label: "Language" }],
4789
+ lexicon: [{ key: "uri", label: "Lexicon URI" }],
4790
+ "mstts:silence": [
4791
+ { key: "typeValue", label: "Silence type" },
4792
+ { key: "value", label: "Value" }
4793
+ ],
4794
+ silence: [
4795
+ { key: "typeValue", label: "Silence type" },
4796
+ { key: "value", label: "Value" }
4797
+ ],
4798
+ "mstts:audioduration": [{ key: "value", label: "Duration" }],
4799
+ "mstts:ttsembedding": [{ key: "speakerProfileId", label: "Speaker profile ID" }],
4800
+ "mstts:embedding": [
4801
+ { key: "id", label: "Embedding ID" },
4802
+ { key: "speakerProfileId", label: "Speaker profile ID" }
4803
+ ],
4804
+ "mstts:voiceconversion": [
4805
+ { key: "url", label: "Source URL" },
4806
+ { key: "profile", label: "Profile" },
4807
+ { key: "speakerProfileId", label: "Speaker profile ID" }
4808
+ ]
4809
+ };
4810
+ function getElementFields(element) {
4811
+ return VISUAL_ELEMENT_FIELDS[elementLabel(element)] ?? [];
4812
+ }
4813
+ function VisualElementInspector({
4814
+ document: document2,
4815
+ element,
4816
+ path,
4817
+ readOnly,
4818
+ commit
4819
+ }) {
4820
+ const fields = getElementFields(element);
4821
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { children: [
4822
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: `<${elementLabel(element)}>` }),
4823
+ fields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "This element is preserved in the visual tree. Edit its attributes in Code mode." }) : fields.map((field) => {
4824
+ const value = element[field.key];
4825
+ const inputValue = value === void 0 ? "" : String(value);
4826
+ const inputId = `ssml-visual-${field.key}`;
4827
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { htmlFor: inputId, children: [
4828
+ field.label,
4829
+ field.multiline ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4830
+ "textarea",
4831
+ {
4832
+ id: inputId,
4833
+ value: inputValue,
4834
+ readOnly,
4835
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
4836
+ }
4837
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4838
+ "input",
4839
+ {
4840
+ id: inputId,
4841
+ value: inputValue,
4842
+ readOnly,
4843
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
4844
+ }
4845
+ )
4846
+ ] }, field.key);
4847
+ })
4848
+ ] });
4849
+ }
4850
+ function getElementAncestors(nodes, path, ancestors = []) {
4851
+ const [index, ...rest] = path;
4852
+ const node = nodes[index];
4853
+ if (!node || !isElement(node)) return ancestors;
4854
+ if (rest.length === 0) return ancestors;
4855
+ return getElementAncestors(node.children ?? [], rest, [...ancestors, node]);
4856
+ }
4857
+ function buildVisualPreview(document2, leaf, selection) {
4858
+ const start = Math.max(0, Math.min(selection.start, leaf.value.length));
4859
+ const end = Math.max(start, Math.min(selection.end, leaf.value.length));
4860
+ const value = start === end ? leaf.value : leaf.value.slice(start, end);
4861
+ const ancestors = getElementAncestors(document2.children ?? [], leaf.path);
4862
+ const children = ancestors.reduceRight(
4863
+ (current, ancestor) => [{ ...ancestor, children: current }],
4864
+ [value]
4865
+ );
4866
+ return (0, import_ssml_core4.buildSsml)({ ...document2, children });
4867
+ }
4868
+ function addDialogTurn(document2, path) {
4869
+ return updateElementAtPath(document2, path, (element) => ({
4870
+ ...element,
4871
+ children: [
4872
+ ...element.children ?? [],
4873
+ { type: "mstts:turn", voice: "en-US-JennyNeural", children: ["New dialog turn"] }
4874
+ ]
4875
+ }));
4876
+ }
4697
4877
  function wrapTextAtPath(document2, path, start, end, tag, attributes) {
4698
4878
  return {
4699
4879
  ...document2,
@@ -4748,6 +4928,7 @@ function VisualSsmlEditor({
4748
4928
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
4749
4929
  const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
4750
4930
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
4931
+ const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
4751
4932
  const diagnostics = (0, import_react3.useMemo)(() => (0, import_ssml_core4.validateAzureSsml)((0, import_ssml_core4.buildSsml)(document2)), [document2]);
4752
4933
  const commit = (nextDocument) => onChange?.(nextDocument);
4753
4934
  const applyWrapper = (tag, attributes) => {
@@ -4782,7 +4963,115 @@ function VisualSsmlEditor({
4782
4963
  isElement(node) ? elementLabel(node) : JSON.stringify(node)
4783
4964
  )) })
4784
4965
  ] }),
4785
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "ssml-editor-visual-form", children: selectedLeaf ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4966
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "ssml-editor-visual-form", children: selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { children: [
4967
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: "Dialog" }),
4968
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Add and edit speaker turns in this dialog." }),
4969
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4970
+ "button",
4971
+ {
4972
+ type: "button",
4973
+ disabled: readOnly,
4974
+ onClick: () => commit(addDialogTurn(document2, selectedPath ?? [])),
4975
+ children: "Add turn"
4976
+ }
4977
+ )
4978
+ ] }) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:turn" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { children: [
4979
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: "Dialog turn" }),
4980
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
4981
+ "Voice",
4982
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4983
+ "input",
4984
+ {
4985
+ value: selectedElement.voice ?? "",
4986
+ readOnly,
4987
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", event.target.value))
4988
+ }
4989
+ )
4990
+ ] }),
4991
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
4992
+ "Speaker",
4993
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4994
+ "input",
4995
+ {
4996
+ value: selectedElement.speaker ?? "",
4997
+ readOnly,
4998
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "speaker", event.target.value))
4999
+ }
5000
+ )
5001
+ ] }),
5002
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
5003
+ "Turn text",
5004
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5005
+ "textarea",
5006
+ {
5007
+ value: selectedElement.children?.filter((child) => typeof child === "string").join("") ?? "",
5008
+ readOnly,
5009
+ onChange: (event) => commit(
5010
+ updateElementAtPath(
5011
+ document2,
5012
+ selectedPath ?? [],
5013
+ (element) => updateElementText(element, event.target.value)
5014
+ )
5015
+ )
5016
+ }
5017
+ )
5018
+ ] })
5019
+ ] }) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:backgroundaudio" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { children: [
5020
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: "Background audio" }),
5021
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
5022
+ "Source URL",
5023
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5024
+ "input",
5025
+ {
5026
+ value: selectedElement.src ?? "",
5027
+ readOnly,
5028
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "src", event.target.value))
5029
+ }
5030
+ )
5031
+ ] }),
5032
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
5033
+ "Volume",
5034
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5035
+ "input",
5036
+ {
5037
+ value: selectedElement.volume === void 0 ? "" : String(selectedElement.volume),
5038
+ readOnly,
5039
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "volume", event.target.value))
5040
+ }
5041
+ )
5042
+ ] }),
5043
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
5044
+ "Fade in",
5045
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5046
+ "input",
5047
+ {
5048
+ value: selectedElement.fadeIn === void 0 ? "" : String(selectedElement.fadeIn),
5049
+ readOnly,
5050
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeIn", event.target.value))
5051
+ }
5052
+ )
5053
+ ] }),
5054
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
5055
+ "Fade out",
5056
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5057
+ "input",
5058
+ {
5059
+ value: selectedElement.fadeOut === void 0 ? "" : String(selectedElement.fadeOut),
5060
+ readOnly,
5061
+ onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeOut", event.target.value))
5062
+ }
5063
+ )
5064
+ ] })
5065
+ ] }) : selectedElement && isElement(selectedElement) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5066
+ VisualElementInspector,
5067
+ {
5068
+ document: document2,
5069
+ element: selectedElement,
5070
+ path: selectedPath ?? [],
5071
+ readOnly,
5072
+ commit
5073
+ }
5074
+ ) : selectedLeaf ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4786
5075
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
4787
5076
  "Text",
4788
5077
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -4822,7 +5111,7 @@ function VisualSsmlEditor({
4822
5111
  "button",
4823
5112
  {
4824
5113
  type: "button",
4825
- onClick: () => onPreviewSelection((0, import_ssml_core4.buildSsml)({ ...document2, children: [selectedLeaf.value] })),
5114
+ onClick: () => onPreviewSelection(buildVisualPreview(document2, selectedLeaf, selection)),
4826
5115
  children: "Preview selection"
4827
5116
  }
4828
5117
  )