@ssml-builder-js/ssml-editor-react 2.13.0 → 2.15.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,27 @@
1
1
  # @ssml-builder-js/ssml-editor-react
2
2
 
3
+ ## 2.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add strict audio merge formats, discriminated synthesis errors, per-event source mappings, abortable URL validation, external audio muxers, and live Visual Editor voice capability warnings.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @ssml-builder-js/ssml-core@2.15.0
13
+
14
+ ## 2.14.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Add safe container-aware audio merging, preflight-validated SSML chunk synthesis with structured progress events, source mapping metadata, controlled URL validation, and voice capability details in the Visual Editor.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies
23
+ - @ssml-builder-js/ssml-core@2.14.0
24
+
3
25
  ## 2.13.0
4
26
 
5
27
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -78,13 +78,18 @@ interface VisualVoiceCatalogEntry {
78
78
  name: string;
79
79
  locale: string;
80
80
  region?: string;
81
+ regions?: readonly string[];
81
82
  styles?: readonly string[];
83
+ supportedTags?: readonly string[];
84
+ unsupportedTags?: readonly string[];
85
+ models?: readonly string[];
82
86
  preview?: boolean;
83
87
  status?: "ga" | "preview" | "deprecated";
84
88
  }
85
89
  interface VoiceSelectorRenderProps {
86
90
  value: string;
87
91
  voices: readonly VisualVoiceCatalogEntry[];
92
+ selectedVoice?: VisualVoiceCatalogEntry;
88
93
  readOnly: boolean;
89
94
  locale: SsmlEditorLocale;
90
95
  onChange: (voice: string) => void;
@@ -110,8 +115,12 @@ interface VisualSsmlEditorProps {
110
115
  voiceLocale?: string;
111
116
  voiceRegion?: string;
112
117
  voiceStyle?: string;
118
+ /** Selected Azure voice model used for live capability validation. */
119
+ voiceModel?: string;
120
+ /** Alias for voiceModel. */
121
+ model?: string;
113
122
  }
114
- declare function VisualSsmlEditor({ document, readOnly, onChange, onPreviewSelection, locale, customInspectors, renderVoiceSelector, voiceCatalog, voiceLocale, voiceRegion, voiceStyle, }: VisualSsmlEditorProps): ReactElement;
123
+ declare function VisualSsmlEditor({ document, readOnly, onChange, onPreviewSelection, locale, customInspectors, renderVoiceSelector, voiceCatalog, voiceLocale, voiceRegion, voiceStyle, voiceModel, model, }: VisualSsmlEditorProps): ReactElement;
115
124
 
116
125
  type SsmlEditorInsertionMode = "insert" | "wrap";
117
126
  interface SsmlEditorInsertionOption {
@@ -297,6 +306,10 @@ interface SsmlEditorProps {
297
306
  voiceRegion?: string;
298
307
  /** Optional style filter for the visual voice selector. */
299
308
  voiceStyle?: string;
309
+ /** Selected Azure voice model used for live capability validation. */
310
+ voiceModel?: string;
311
+ /** Alias for voiceModel. */
312
+ model?: string;
300
313
  /** Candidate style values shown by the built-in emotion insertion. */
301
314
  emotionStyles?: readonly string[];
302
315
  /** Class name applied to the editor container. */
package/dist/index.d.ts CHANGED
@@ -78,13 +78,18 @@ interface VisualVoiceCatalogEntry {
78
78
  name: string;
79
79
  locale: string;
80
80
  region?: string;
81
+ regions?: readonly string[];
81
82
  styles?: readonly string[];
83
+ supportedTags?: readonly string[];
84
+ unsupportedTags?: readonly string[];
85
+ models?: readonly string[];
82
86
  preview?: boolean;
83
87
  status?: "ga" | "preview" | "deprecated";
84
88
  }
85
89
  interface VoiceSelectorRenderProps {
86
90
  value: string;
87
91
  voices: readonly VisualVoiceCatalogEntry[];
92
+ selectedVoice?: VisualVoiceCatalogEntry;
88
93
  readOnly: boolean;
89
94
  locale: SsmlEditorLocale;
90
95
  onChange: (voice: string) => void;
@@ -110,8 +115,12 @@ interface VisualSsmlEditorProps {
110
115
  voiceLocale?: string;
111
116
  voiceRegion?: string;
112
117
  voiceStyle?: string;
118
+ /** Selected Azure voice model used for live capability validation. */
119
+ voiceModel?: string;
120
+ /** Alias for voiceModel. */
121
+ model?: string;
113
122
  }
114
- declare function VisualSsmlEditor({ document, readOnly, onChange, onPreviewSelection, locale, customInspectors, renderVoiceSelector, voiceCatalog, voiceLocale, voiceRegion, voiceStyle, }: VisualSsmlEditorProps): ReactElement;
123
+ declare function VisualSsmlEditor({ document, readOnly, onChange, onPreviewSelection, locale, customInspectors, renderVoiceSelector, voiceCatalog, voiceLocale, voiceRegion, voiceStyle, voiceModel, model, }: VisualSsmlEditorProps): ReactElement;
115
124
 
116
125
  type SsmlEditorInsertionMode = "insert" | "wrap";
117
126
  interface SsmlEditorInsertionOption {
@@ -297,6 +306,10 @@ interface SsmlEditorProps {
297
306
  voiceRegion?: string;
298
307
  /** Optional style filter for the visual voice selector. */
299
308
  voiceStyle?: string;
309
+ /** Selected Azure voice model used for live capability validation. */
310
+ voiceModel?: string;
311
+ /** Alias for voiceModel. */
312
+ model?: string;
300
313
  /** Candidate style values shown by the built-in emotion insertion. */
301
314
  emotionStyles?: readonly string[];
302
315
  /** Class name applied to the editor container. */
package/dist/index.js CHANGED
@@ -5035,29 +5035,107 @@ function localizedElementLabel(element, locale) {
5035
5035
  function filteredVoices(voiceCatalog, locale, region, style) {
5036
5036
  return (voiceCatalog ?? []).filter((voice) => {
5037
5037
  if (locale && voice.locale.toLowerCase() !== locale.toLowerCase()) return false;
5038
- if (region && voice.region && voice.region.toLowerCase() !== region.toLowerCase()) return false;
5038
+ const regions = [voice.region, ...voice.regions ?? []].filter(
5039
+ (candidate) => Boolean(candidate)
5040
+ );
5041
+ if (region && regions.length > 0 && !regions.some((candidate) => candidate.toLowerCase() === region.toLowerCase()))
5042
+ return false;
5039
5043
  if (style && !voice.styles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase())) return false;
5040
5044
  return true;
5041
5045
  });
5042
5046
  }
5043
- function DefaultVoiceSelector({ value, voices, readOnly, locale, onChange }) {
5044
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
5045
- "select",
5047
+ function VoiceCapabilityMatrix({
5048
+ voice,
5049
+ locale
5050
+ }) {
5051
+ const status = voice.status ?? (voice.preview ? "preview" : void 0);
5052
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "ssml-editor-voice-capabilities", "data-voice-capabilities": "", children: [
5053
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
5054
+ locale === "ja" ? "\u30B9\u30C6\u30FC\u30BF\u30B9" : "Status",
5055
+ ": ",
5056
+ status?.toUpperCase() ?? (locale === "ja" ? "GA" : "GA"),
5057
+ status === "preview" || status === "deprecated" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { role: "status", "aria-label": status, className: "ssml-editor-voice-warning-badge", children: status === "preview" ? "\u26A0 Preview" : "\u26A0 Deprecated" }) : null
5058
+ ] }),
5059
+ (voice.regions?.length || voice.region) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
5060
+ locale === "ja" ? "\u30EA\u30FC\u30B8\u30E7\u30F3" : "Regions",
5061
+ ":",
5062
+ " ",
5063
+ [...voice.regions ?? [], ...voice.region ? [voice.region] : []].filter((region, index, all) => all.indexOf(region) === index).join(", ")
5064
+ ] }),
5065
+ voice.supportedTags && voice.supportedTags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
5066
+ locale === "ja" ? "\u5BFE\u5FDC\u30BF\u30B0" : "Supported tags",
5067
+ ": ",
5068
+ voice.supportedTags.join(", ")
5069
+ ] }),
5070
+ voice.unsupportedTags && voice.unsupportedTags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "ssml-editor-voice-unsupported", children: [
5071
+ locale === "ja" ? "\u975E\u5BFE\u5FDC\u30BF\u30B0" : "Unsupported tags",
5072
+ ": ",
5073
+ voice.unsupportedTags.join(", ")
5074
+ ] })
5075
+ ] });
5076
+ }
5077
+ function diagnosticMatchesElement(diagnostic, element) {
5078
+ const name = elementLabel(element).toLowerCase().replace("expressas", "mstts:express-as");
5079
+ if (diagnostic.code === "azure-unsupported-style") return name === "mstts:express-as" || name === "express-as";
5080
+ if (diagnostic.code === "azure-unsupported-tag-for-voice") {
5081
+ return diagnostic.message.toLowerCase().includes(`<${name}>`) || diagnostic.message.toLowerCase().includes(`<${name.replace("mstts:", "")}>`);
5082
+ }
5083
+ if (diagnostic.code === "azure-locale-mismatch" || diagnostic.code === "azure-unsupported-model-for-voice")
5084
+ return name === "voice" || name === "mstts:turn";
5085
+ return false;
5086
+ }
5087
+ function elementWarnings(diagnostics, element) {
5088
+ return diagnostics.filter((diagnostic) => diagnosticMatchesElement(diagnostic, element));
5089
+ }
5090
+ function fieldWarnings(diagnostics, element, field) {
5091
+ return elementWarnings(diagnostics, element).filter((diagnostic) => {
5092
+ if (diagnostic.code === "azure-unsupported-style") return field.key === "style";
5093
+ return field.key === "name" || field.key === "voice";
5094
+ });
5095
+ }
5096
+ function WarningBadge({ messages }) {
5097
+ if (messages.length === 0) return null;
5098
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5099
+ "span",
5046
5100
  {
5047
- "aria-label": locale === "ja" ? "\u97F3\u58F0" : "Voice",
5048
- value,
5049
- disabled: readOnly,
5050
- onChange: (event) => onChange(event.target.value),
5051
- children: [
5052
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: locale === "ja" ? "\u97F3\u58F0\u3092\u9078\u629E" : "Select a voice" }),
5053
- voices.map((voice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("option", { value: voice.name, children: [
5054
- voice.name,
5055
- voice.preview || voice.status === "preview" ? " (preview)" : ""
5056
- ] }, voice.name))
5057
- ]
5101
+ role: "status",
5102
+ "aria-label": messages.map((message) => message.message).join(" "),
5103
+ title: messages.map((message) => message.message).join(" "),
5104
+ "data-ssml-warning-badge": "",
5105
+ "data-testid": "ssml-editor-warning-badge",
5106
+ className: "ssml-editor-warning-badge",
5107
+ children: "\u26A0"
5058
5108
  }
5059
5109
  );
5060
5110
  }
5111
+ function DefaultVoiceSelector({
5112
+ value,
5113
+ voices,
5114
+ selectedVoice,
5115
+ readOnly,
5116
+ locale,
5117
+ onChange
5118
+ }) {
5119
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5120
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
5121
+ "select",
5122
+ {
5123
+ "aria-label": locale === "ja" ? "\u97F3\u58F0" : "Voice",
5124
+ value,
5125
+ disabled: readOnly,
5126
+ onChange: (event) => onChange(event.target.value),
5127
+ children: [
5128
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: locale === "ja" ? "\u97F3\u58F0\u3092\u9078\u629E" : "Select a voice" }),
5129
+ voices.map((voice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("option", { value: voice.name, children: [
5130
+ voice.name,
5131
+ voice.preview || voice.status === "preview" ? " (preview)" : ""
5132
+ ] }, voice.name))
5133
+ ]
5134
+ }
5135
+ ),
5136
+ selectedVoice ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(VoiceCapabilityMatrix, { voice: selectedVoice, locale }) : null
5137
+ ] });
5138
+ }
5061
5139
  function VisualElementInspector({
5062
5140
  document: document2,
5063
5141
  element,
@@ -5070,16 +5148,24 @@ function VisualElementInspector({
5070
5148
  voiceCatalog,
5071
5149
  voiceLocale,
5072
5150
  voiceRegion,
5073
- voiceStyle
5151
+ voiceStyle,
5152
+ diagnostics
5074
5153
  }) {
5075
5154
  if (customInspector) {
5076
5155
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: customInspector({ document: document2, element, path, readOnly, onChange: commit, locale }) });
5077
5156
  }
5078
5157
  const fields = getElementFields(element);
5079
5158
  const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
5159
+ const selectedVoice = voiceCatalog?.find(
5160
+ (voice) => voice.name === (element.type === "voice" ? element.name : void 0)
5161
+ );
5080
5162
  const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
5081
5163
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { children: [
5082
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: `<${elementLabel(element)}>` }),
5164
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("legend", { children: [
5165
+ `<${elementLabel(element)}>`,
5166
+ " ",
5167
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WarningBadge, { messages: elementWarnings(diagnostics, element) })
5168
+ ] }),
5083
5169
  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) => {
5084
5170
  const value = element[field.key];
5085
5171
  const inputValue = value === void 0 ? "" : String(value);
@@ -5119,9 +5205,11 @@ function VisualElementInspector({
5119
5205
  url: "\u97F3\u58F0\u5909\u63DB URL",
5120
5206
  profile: "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB"
5121
5207
  }[field.key] ?? field.label : field.label,
5208
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WarningBadge, { messages: fieldWarnings(diagnostics, element, field) }),
5122
5209
  field.key === "name" && elementLabel(element) === "voice" && voiceCatalog ? renderSelector({
5123
5210
  value: inputValue,
5124
5211
  voices: availableVoices,
5212
+ selectedVoice,
5125
5213
  readOnly,
5126
5214
  locale,
5127
5215
  onChange: (value2) => commit(updateOptionalElementProperty(document2, path, field.key, value2))
@@ -5198,20 +5286,25 @@ function TreeNode({
5198
5286
  node,
5199
5287
  path,
5200
5288
  selectedPath,
5201
- onSelect
5289
+ onSelect,
5290
+ getWarnings
5202
5291
  }) {
5203
5292
  if (!isElement(node)) return null;
5204
5293
  const name = elementLabel(node);
5205
5294
  const isSelected = selectedPath?.join(".") === path.join(".");
5206
5295
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
5207
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path), children: `<${name}>` }),
5296
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path), children: [
5297
+ `<${name}>`,
5298
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WarningBadge, { messages: getWarnings(node) })
5299
+ ] }),
5208
5300
  (node.children ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { children: node.children?.map((child, index) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5209
5301
  TreeNode,
5210
5302
  {
5211
5303
  node: child,
5212
5304
  path: [...path, index],
5213
5305
  selectedPath,
5214
- onSelect
5306
+ onSelect,
5307
+ getWarnings
5215
5308
  },
5216
5309
  `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`
5217
5310
  )) })
@@ -5228,7 +5321,9 @@ function VisualSsmlEditor({
5228
5321
  voiceCatalog,
5229
5322
  voiceLocale,
5230
5323
  voiceRegion,
5231
- voiceStyle
5324
+ voiceStyle,
5325
+ voiceModel,
5326
+ model
5232
5327
  }) {
5233
5328
  const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
5234
5329
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
@@ -5236,7 +5331,27 @@ function VisualSsmlEditor({
5236
5331
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
5237
5332
  const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
5238
5333
  const selectedCustomInspector = selectedElement && isElement(selectedElement) ? customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type] : void 0;
5239
- const diagnostics = (0, import_react3.useMemo)(() => (0, import_ssml_core4.validateAzureSsml)((0, import_ssml_core4.buildSsml)(document2)), [document2]);
5334
+ const validationOptions = (0, import_react3.useMemo)(
5335
+ () => ({
5336
+ model: voiceModel ?? model,
5337
+ customVoiceDefinitions: voiceCatalog?.map((voice) => ({
5338
+ name: voice.name,
5339
+ locale: voice.locale,
5340
+ regions: voice.regions ?? (voice.region ? [voice.region] : void 0),
5341
+ styles: voice.styles,
5342
+ supportedTags: voice.supportedTags,
5343
+ unsupportedTags: voice.unsupportedTags,
5344
+ models: voice.models,
5345
+ status: voice.status ?? (voice.preview ? "preview" : "ga")
5346
+ }))
5347
+ }),
5348
+ [model, voiceCatalog, voiceModel]
5349
+ );
5350
+ const diagnostics = (0, import_react3.useMemo)(
5351
+ () => (0, import_ssml_core4.validateAzureSsml)((0, import_ssml_core4.buildSsml)(document2), validationOptions),
5352
+ [document2, validationOptions]
5353
+ );
5354
+ const getWarnings = (element) => elementWarnings(diagnostics, element);
5240
5355
  const commit = (nextDocument) => onChange?.(nextDocument);
5241
5356
  const applyWrapper = (tag, attributes) => {
5242
5357
  if (readOnly || !selectedLeaf) return;
@@ -5265,7 +5380,8 @@ function VisualSsmlEditor({
5265
5380
  node,
5266
5381
  path: [index],
5267
5382
  selectedPath,
5268
- onSelect: setSelectedPath
5383
+ onSelect: setSelectedPath,
5384
+ getWarnings
5269
5385
  },
5270
5386
  isElement(node) ? elementLabel(node) : JSON.stringify(node)
5271
5387
  )) })
@@ -5293,9 +5409,11 @@ function VisualSsmlEditor({
5293
5409
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { children: localizedElementLabel(selectedElement, locale) }),
5294
5410
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { htmlFor: "ssml-visual-turn-voice", children: [
5295
5411
  locale === "ja" ? "\u97F3\u58F0" : "Voice",
5412
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WarningBadge, { messages: elementWarnings(diagnostics, selectedElement) }),
5296
5413
  voiceCatalog ? (renderVoiceSelector ?? DefaultVoiceSelector)({
5297
5414
  value: selectedElement.voice ?? "",
5298
5415
  voices: filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle),
5416
+ selectedVoice: voiceCatalog?.find((voice) => voice.name === selectedElement.voice),
5299
5417
  readOnly,
5300
5418
  locale,
5301
5419
  onChange: (value) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", value))
@@ -5397,7 +5515,8 @@ function VisualSsmlEditor({
5397
5515
  voiceCatalog,
5398
5516
  voiceLocale,
5399
5517
  voiceRegion,
5400
- voiceStyle
5518
+ voiceStyle,
5519
+ diagnostics
5401
5520
  }
5402
5521
  ) : selectedLeaf ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5403
5522
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { children: [
@@ -6042,6 +6161,8 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
6042
6161
  voiceLocale,
6043
6162
  voiceRegion,
6044
6163
  voiceStyle,
6164
+ voiceModel,
6165
+ model,
6045
6166
  emotionStyles,
6046
6167
  className,
6047
6168
  style,
@@ -6490,7 +6611,9 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
6490
6611
  voiceCatalog,
6491
6612
  voiceLocale,
6492
6613
  voiceRegion,
6493
- voiceStyle
6614
+ voiceStyle,
6615
+ voiceModel,
6616
+ model
6494
6617
  }
6495
6618
  ) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight }, children: [
6496
6619
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(