ssml-builder-js 2.15.0 → 2.17.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
@@ -1743,6 +1743,8 @@ function InsertionPopover({
1743
1743
  toolbarButtonStyle,
1744
1744
  emptyOptionsMessage,
1745
1745
  isReadOnly,
1746
+ disabled = false,
1747
+ disabledReason,
1746
1748
  isOpen,
1747
1749
  menuPosition,
1748
1750
  menuRef,
@@ -1760,7 +1762,7 @@ function InsertionPopover({
1760
1762
  role: "menuitem",
1761
1763
  style: editorStyles.toolbarOption,
1762
1764
  title: option.descriptions?.[language] ?? insertion.descriptions[language],
1763
- disabled: isReadOnly,
1765
+ disabled: isReadOnly || disabled,
1764
1766
  onMouseDown: (event) => event.preventDefault(),
1765
1767
  onClick: () => {
1766
1768
  if (!isReadOnly) {
@@ -1789,11 +1791,12 @@ function InsertionPopover({
1789
1791
  {
1790
1792
  type: "button",
1791
1793
  style: toolbarButtonStyle,
1792
- title: getInsertionTitle(insertion, language),
1794
+ title: disabledReason ?? getInsertionTitle(insertion, language),
1793
1795
  "aria-label": insertion.labels[language],
1794
1796
  "aria-haspopup": "menu",
1795
1797
  "aria-expanded": isOpen,
1796
1798
  "aria-controls": isOpen ? `ssml-editor-popover-${insertion.id}` : void 0,
1799
+ disabled: isReadOnly || disabled,
1797
1800
  onClick: (event) => onToggle(event.currentTarget)
1798
1801
  },
1799
1802
  showToolbarIcons && /* @__PURE__ */ React.createElement("span", { style: editorStyles.toolbarIcon, "aria-hidden": "true" }, insertion.icon),
@@ -3144,6 +3147,7 @@ function tokenizeElements(source) {
3144
3147
  if (parent) parent.childElementCount += 1;
3145
3148
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
3146
3149
  const tokenName = nameMatch[1];
3150
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
3147
3151
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
3148
3152
  tokens.push({
3149
3153
  attributes,
@@ -3154,12 +3158,14 @@ function tokenizeElements(source) {
3154
3158
  parentName: parent?.name,
3155
3159
  parentVoiceName,
3156
3160
  selfClosing,
3157
- start
3161
+ start,
3162
+ path
3158
3163
  });
3159
3164
  if (!selfClosing) {
3160
3165
  openElements.push({
3161
3166
  childElementCount: 0,
3162
3167
  name: tokenName,
3168
+ path,
3163
3169
  voiceName: tokenVoiceName
3164
3170
  });
3165
3171
  }
@@ -3172,13 +3178,14 @@ function location(source, offset) {
3172
3178
  const line = before.split("\n").length;
3173
3179
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
3174
3180
  }
3175
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2) {
3181
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code2, metadata = {}) {
3176
3182
  diagnostics.push({
3177
3183
  ...location(source, offset),
3178
3184
  message,
3179
3185
  severity,
3180
3186
  source: "ssml-static-validator",
3181
- ...code2 ? { code: code2 } : {}
3187
+ ...code2 ? { code: code2 } : {},
3188
+ ...metadata
3182
3189
  });
3183
3190
  }
3184
3191
  function isSupportedProsodyRate(value) {
@@ -3641,9 +3648,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
3641
3648
  for (const token of tokens) {
3642
3649
  const tokenName = token.name.toLowerCase();
3643
3650
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
3651
+ const tokenDiagnosticStart = diagnostics.length;
3644
3652
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
3645
3653
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
3646
3654
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
3655
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
3647
3656
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
3648
3657
  addDiagnostic(
3649
3658
  diagnostics,
@@ -3665,12 +3674,30 @@ function urlAttributes(token) {
3665
3674
  return value === void 0 ? [] : [{ attribute, value }];
3666
3675
  });
3667
3676
  }
3677
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
3678
+ const attributes = [...token.attributes.keys()];
3679
+ for (const diagnostic of diagnostics.slice(startIndex)) {
3680
+ const attributeName = attributes.find(
3681
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
3682
+ );
3683
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
3684
+ Object.assign(diagnostic, {
3685
+ range: { start: token.start, end: token.end + 1 },
3686
+ tagName: token.name,
3687
+ ...attributeName ? { attributeName } : {},
3688
+ ...voiceName ? { voiceName } : {},
3689
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
3690
+ nodePath,
3691
+ targetNodePath: [...token.path]
3692
+ });
3693
+ }
3694
+ }
3668
3695
  function validateAzureSsml(ssml, options = {}) {
3669
3696
  const diagnostics = validateAzureSsmlStatic(ssml, options);
3670
3697
  const validator = options.urlValidator ?? options.customUrlValidator;
3671
3698
  if (!validator || typeof ssml !== "string") return diagnostics;
3672
3699
  const runnerOptions = options.urlValidation ?? {};
3673
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
3700
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
3674
3701
  ...runnerOptions,
3675
3702
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
3676
3703
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -3695,21 +3722,25 @@ function validateAzureSsml(ssml, options = {}) {
3695
3722
  const valid = typeof result === "boolean" ? result : result.valid;
3696
3723
  if (!valid) {
3697
3724
  const reason = typeof result === "boolean" ? void 0 : result.reason;
3725
+ const diagnosticStart = diagnostics.length;
3698
3726
  addDiagnostic(
3699
3727
  diagnostics,
3700
3728
  ssml,
3701
3729
  token.start,
3702
3730
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
3703
3731
  );
3732
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
3704
3733
  }
3705
3734
  } catch (error) {
3706
3735
  const reason = error instanceof Error ? error.message : String(error);
3736
+ const diagnosticStart = diagnostics.length;
3707
3737
  addDiagnostic(
3708
3738
  diagnostics,
3709
3739
  ssml,
3710
3740
  token.start,
3711
3741
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
3712
3742
  );
3743
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
3713
3744
  }
3714
3745
  })
3715
3746
  );
@@ -3719,7 +3750,9 @@ var AZURE_VOICE_CATALOG_METADATA = {
3719
3750
  apiVersion: "2025-10-01",
3720
3751
  generatedAt: "2026-08-28T00:00:00.000Z",
3721
3752
  regions: [],
3722
- voiceCount: AZURE_VOICE_DEFINITIONS.length
3753
+ voiceCount: AZURE_VOICE_DEFINITIONS.length,
3754
+ expiresAt: "2026-09-04T00:00:00.000Z",
3755
+ regionDiffs: {}
3723
3756
  };
3724
3757
 
3725
3758
  // packages/ssml-editor-react/src/clearSsmlDocument.ts
@@ -6856,6 +6889,21 @@ function filteredVoices(voiceCatalog, locale, region, style) {
6856
6889
  return true;
6857
6890
  });
6858
6891
  }
6892
+ function voiceForPath(document2, path, voiceCatalog) {
6893
+ if (!path || !voiceCatalog) return void 0;
6894
+ const ancestors = getElementAncestors(document2.children ?? [], path);
6895
+ const node = getNodeAtPath(document2.children ?? [], path);
6896
+ const candidates = [...ancestors, ...node && isElement(node) ? [node] : []].reverse();
6897
+ const voiceName = candidates.find((element) => element.type === "voice")?.name ?? candidates.find((element) => element.type === "mstts:turn")?.voice;
6898
+ return voiceName ? voiceCatalog.find((voice) => voice.name.toLowerCase() === voiceName.toLowerCase()) : void 0;
6899
+ }
6900
+ function isVisualTagSupported(tagName, voice) {
6901
+ if (!voice) return true;
6902
+ const tag = tagName.toLowerCase();
6903
+ const unsupported = new Set((voice.unsupportedTags ?? []).map((candidate) => candidate.toLowerCase()));
6904
+ const supported = voice.supportedTags?.map((candidate) => candidate.toLowerCase());
6905
+ return !unsupported.has(tag) && (supported === void 0 || supported.includes(tag));
6906
+ }
6859
6907
  function VoiceCapabilityMatrix({
6860
6908
  voice,
6861
6909
  locale
@@ -6930,10 +6978,12 @@ function VisualElementInspector({
6930
6978
  voiceLocale,
6931
6979
  voiceRegion,
6932
6980
  voiceStyle,
6933
- diagnostics
6981
+ diagnostics,
6982
+ voiceDefinition
6934
6983
  }) {
6984
+ const tagSupported = isVisualTagSupported(elementLabel(element), voiceDefinition);
6935
6985
  if (customInspector) {
6936
- return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly, onChange: commit, locale }));
6986
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly: readOnly || !tagSupported, onChange: commit, locale }));
6937
6987
  }
6938
6988
  const fields = getElementFields(element);
6939
6989
  const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
@@ -6941,7 +6991,7 @@ function VisualElementInspector({
6941
6991
  (voice) => voice.name === (element.type === "voice" ? element.name : void 0)
6942
6992
  );
6943
6993
  const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
6944
- return /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`, " ", /* @__PURE__ */ React.createElement(WarningBadge, { messages: elementWarnings(diagnostics, 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) => {
6994
+ return /* @__PURE__ */ React.createElement("fieldset", { disabled: readOnly || !tagSupported, style: !tagSupported ? { border: "1px solid #c0392b" } : void 0 }, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`, " ", /* @__PURE__ */ React.createElement(WarningBadge, { messages: elementWarnings(diagnostics, 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) => {
6945
6995
  const value = element[field.key];
6946
6996
  const inputValue = value === void 0 ? "" : String(value);
6947
6997
  const inputId = `ssml-visual-${field.key}`;
@@ -6990,6 +7040,7 @@ function VisualElementInspector({
6990
7040
  {
6991
7041
  id: inputId,
6992
7042
  value: inputValue,
7043
+ disabled: readOnly || !tagSupported,
6993
7044
  readOnly,
6994
7045
  onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
6995
7046
  }
@@ -6998,6 +7049,7 @@ function VisualElementInspector({
6998
7049
  {
6999
7050
  id: inputId,
7000
7051
  value: inputValue,
7052
+ disabled: readOnly || !tagSupported,
7001
7053
  readOnly,
7002
7054
  onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
7003
7055
  }
@@ -7061,18 +7113,27 @@ function TreeNode({
7061
7113
  }) {
7062
7114
  if (!isElement(node)) return null;
7063
7115
  const name = elementLabel(node);
7116
+ const messages = getWarnings(node);
7064
7117
  const isSelected = selectedPath?.join(".") === path.join(".");
7065
- return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`, /* @__PURE__ */ React.createElement(WarningBadge, { messages: getWarnings(node) })), (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
7066
- TreeNode,
7118
+ return /* @__PURE__ */ React.createElement(
7119
+ "li",
7067
7120
  {
7068
- key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
7069
- node: child,
7070
- path: [...path, index],
7071
- selectedPath,
7072
- onSelect,
7073
- getWarnings
7074
- }
7075
- ))));
7121
+ "data-ssml-diagnostic": messages.length > 0 ? "error" : void 0,
7122
+ style: messages.length > 0 ? { border: "1px solid #c0392b", borderRadius: "0.25rem" } : void 0
7123
+ },
7124
+ /* @__PURE__ */ React.createElement("button", { type: "button", "aria-current": isSelected ? "true" : void 0, onClick: () => onSelect(path) }, `<${name}>`, /* @__PURE__ */ React.createElement(WarningBadge, { messages: getWarnings(node) })),
7125
+ (node.children ?? []).length > 0 && /* @__PURE__ */ React.createElement("ul", null, node.children?.map((child, index) => /* @__PURE__ */ React.createElement(
7126
+ TreeNode,
7127
+ {
7128
+ key: `${path.join(".")}/${isElement(child) ? elementLabel(child) : JSON.stringify(child)}`,
7129
+ node: child,
7130
+ path: [...path, index],
7131
+ selectedPath,
7132
+ onSelect,
7133
+ getWarnings
7134
+ }
7135
+ )))
7136
+ );
7076
7137
  }
7077
7138
  function VisualSsmlEditor({
7078
7139
  document: document2,
@@ -7087,13 +7148,16 @@ function VisualSsmlEditor({
7087
7148
  voiceRegion,
7088
7149
  voiceStyle,
7089
7150
  voiceModel,
7090
- model
7151
+ model,
7152
+ voiceCatalogMetadata
7091
7153
  }) {
7092
7154
  const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
7093
7155
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
7094
7156
  const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
7095
7157
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
7096
7158
  const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
7159
+ const activeVoice = voiceForPath(document2, selectedPath ?? selectedLeaf?.path, voiceCatalog);
7160
+ const staleCatalog = voiceCatalogMetadata?.expiresAt ? Date.parse(voiceCatalogMetadata.expiresAt) <= Date.now() : false;
7097
7161
  const selectedCustomInspector = selectedElement && isElement(selectedElement) ? customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type] : void 0;
7098
7162
  const validationOptions = (0, import_react3.useMemo)(
7099
7163
  () => ({
@@ -7123,7 +7187,7 @@ function VisualSsmlEditor({
7123
7187
  const end = selection.start === selection.end ? selectedLeaf.value.length : selection.end;
7124
7188
  commit(wrapTextAtPath(document2, selectedLeaf.path, start, end, tag, attributes));
7125
7189
  };
7126
- return /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual", "data-ssml-editor-visual": "" }, /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-breadcrumb" }, /* @__PURE__ */ React.createElement("legend", null, "SSML structure breadcrumb"), /* @__PURE__ */ React.createElement("span", null, "<speak>"), selectedLeaf?.ancestors.map((ancestor, index) => /* @__PURE__ */ React.createElement("span", { key: selectedLeaf?.ancestors.slice(0, index + 1).join("/") }, " / <", ancestor, ">")), selectedPath && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSelectedPath(null) }, "Clear parent")), diagnostics.length > 0 && /* @__PURE__ */ React.createElement("div", { role: "alert", "aria-invalid": "true", className: "ssml-editor-visual-errors" }, diagnostics.map((diagnostic) => /* @__PURE__ */ React.createElement("div", { key: `${diagnostic.code}-${diagnostic.message}` }, diagnostic.message))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-layout" }, /* @__PURE__ */ React.createElement("nav", { "aria-label": "SSML structure tree", className: "ssml-editor-visual-tree" }, /* @__PURE__ */ React.createElement("strong", null, "Structure"), /* @__PURE__ */ React.createElement("ul", null, document2.children?.map((node, index) => /* @__PURE__ */ React.createElement(
7190
+ return /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual", "data-ssml-editor-visual": "" }, /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-breadcrumb" }, /* @__PURE__ */ React.createElement("legend", null, "SSML structure breadcrumb"), /* @__PURE__ */ React.createElement("span", null, "<speak>"), selectedLeaf?.ancestors.map((ancestor, index) => /* @__PURE__ */ React.createElement("span", { key: selectedLeaf?.ancestors.slice(0, index + 1).join("/") }, " / <", ancestor, ">")), selectedPath && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSelectedPath(null) }, "Clear parent")), diagnostics.length > 0 && /* @__PURE__ */ React.createElement("div", { role: "alert", "aria-invalid": "true", className: "ssml-editor-visual-errors" }, diagnostics.map((diagnostic) => /* @__PURE__ */ React.createElement("div", { key: `${diagnostic.code}-${diagnostic.message}` }, diagnostic.message))), staleCatalog && /* @__PURE__ */ React.createElement("div", { role: "status", className: "ssml-editor-visual-catalog-warning" }, locale === "ja" ? "\u97F3\u58F0\u30AB\u30BF\u30ED\u30B0\u304C\u53E4\u304F\u306A\u3063\u3066\u3044\u307E\u3059\u3002\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "The voice catalog is stale. Refresh it before synthesizing."), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-layout" }, /* @__PURE__ */ React.createElement("nav", { "aria-label": "SSML structure tree", className: "ssml-editor-visual-tree" }, /* @__PURE__ */ React.createElement("strong", null, "Structure"), /* @__PURE__ */ React.createElement("ul", null, document2.children?.map((node, index) => /* @__PURE__ */ React.createElement(
7127
7191
  TreeNode,
7128
7192
  {
7129
7193
  key: isElement(node) ? elementLabel(node) : JSON.stringify(node),
@@ -7137,14 +7201,14 @@ function VisualSsmlEditor({
7137
7201
  document: document2,
7138
7202
  element: selectedElement,
7139
7203
  path: selectedPath ?? [],
7140
- readOnly,
7204
+ readOnly: readOnly || !isVisualTagSupported(elementLabel(selectedElement), activeVoice),
7141
7205
  onChange: commit,
7142
7206
  locale
7143
7207
  }) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, localizedElementLabel(selectedElement, locale)), /* @__PURE__ */ React.createElement("p", null, SSML_ELEMENT_COPY[locale][elementLabel(selectedElement)]?.description), /* @__PURE__ */ React.createElement(
7144
7208
  "button",
7145
7209
  {
7146
7210
  type: "button",
7147
- disabled: readOnly,
7211
+ disabled: readOnly || !isVisualTagSupported("mstts:dialog", activeVoice),
7148
7212
  onClick: () => commit(addDialogTurn(document2, selectedPath ?? []))
7149
7213
  },
7150
7214
  locale === "ja" ? "\u30BF\u30FC\u30F3\u3092\u8FFD\u52A0" : "Add turn"
@@ -7226,7 +7290,8 @@ function VisualSsmlEditor({
7226
7290
  voiceLocale,
7227
7291
  voiceRegion,
7228
7292
  voiceStyle,
7229
- diagnostics
7293
+ diagnostics,
7294
+ voiceDefinition: activeVoice
7230
7295
  }
7231
7296
  ) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
7232
7297
  "textarea",
@@ -7236,19 +7301,43 @@ function VisualSsmlEditor({
7236
7301
  onSelect: (event) => setSelection({ start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd }),
7237
7302
  onChange: (event) => commit(updateTextAtPath(document2, selectedLeaf.path, event.target.value))
7238
7303
  }
7239
- )), /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-actions" }, /* @__PURE__ */ React.createElement("legend", null, "Apply SSML formatting"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { rate: "slow" }) }, "Rate"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("prosody", { pitch: "high" }) }, "Pitch"), /* @__PURE__ */ React.createElement(
7304
+ )), /* @__PURE__ */ React.createElement("fieldset", { className: "ssml-editor-visual-actions" }, /* @__PURE__ */ React.createElement("legend", null, "Apply SSML formatting"), /* @__PURE__ */ React.createElement(
7240
7305
  "button",
7241
7306
  {
7242
7307
  type: "button",
7243
- disabled: readOnly,
7308
+ disabled: readOnly || !isVisualTagSupported("prosody", activeVoice),
7309
+ onClick: () => applyWrapper("prosody", { rate: "slow" })
7310
+ },
7311
+ "Rate"
7312
+ ), /* @__PURE__ */ React.createElement(
7313
+ "button",
7314
+ {
7315
+ type: "button",
7316
+ disabled: readOnly || !isVisualTagSupported("prosody", activeVoice),
7317
+ onClick: () => applyWrapper("prosody", { pitch: "high" })
7318
+ },
7319
+ "Pitch"
7320
+ ), /* @__PURE__ */ React.createElement(
7321
+ "button",
7322
+ {
7323
+ type: "button",
7324
+ disabled: readOnly || !isVisualTagSupported("mstts:express-as", activeVoice),
7244
7325
  onClick: () => applyWrapper("mstts:express-as", { style: "cheerful" })
7245
7326
  },
7246
7327
  "Emotion"
7247
- ), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: readOnly, onClick: () => applyWrapper("break", { time: "500ms" }) }, "Pause"), /* @__PURE__ */ React.createElement(
7328
+ ), /* @__PURE__ */ React.createElement(
7248
7329
  "button",
7249
7330
  {
7250
7331
  type: "button",
7251
- disabled: readOnly,
7332
+ disabled: readOnly || !isVisualTagSupported("break", activeVoice),
7333
+ onClick: () => applyWrapper("break", { time: "500ms" })
7334
+ },
7335
+ "Pause"
7336
+ ), /* @__PURE__ */ React.createElement(
7337
+ "button",
7338
+ {
7339
+ type: "button",
7340
+ disabled: readOnly || !isVisualTagSupported("phoneme", activeVoice),
7252
7341
  onClick: () => applyWrapper("phoneme", { alphabet: "ipa", ph: selectedLeaf.value })
7253
7342
  },
7254
7343
  "Pronunciation"
@@ -7267,6 +7356,23 @@ var UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
7267
7356
  var TIMING_POPOVER_TAGS = /* @__PURE__ */ new Set(["break", "mstts:silence", "mstts:audioduration"]);
7268
7357
  var PROSODY_POPOVER_TAGS = /* @__PURE__ */ new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
7269
7358
  var TEXT_POPOVER_TAGS = /* @__PURE__ */ new Set(["sub", "say-as", "phoneme", "w", "lang"]);
7359
+ function isVoiceTagSupported(tagName, voice) {
7360
+ if (!tagName || !voice) return true;
7361
+ const tag = tagName.toLowerCase();
7362
+ const unsupported = new Set((voice.unsupportedTags ?? []).map((candidate) => candidate.toLowerCase()));
7363
+ const supported = voice.supportedTags?.map((candidate) => candidate.toLowerCase());
7364
+ return !unsupported.has(tag) && (supported === void 0 || supported.includes(tag));
7365
+ }
7366
+ function findFirstVoiceName(nodes) {
7367
+ for (const node of nodes) {
7368
+ if (typeof node === "string" || node.type === "text") continue;
7369
+ if (node.type === "voice" && node.name) return node.name;
7370
+ if (node.type === "mstts:turn" && node.voice) return node.voice;
7371
+ const nested = findFirstVoiceName(node.children ?? []);
7372
+ if (nested) return nested;
7373
+ }
7374
+ return void 0;
7375
+ }
7270
7376
  function localizedText(value) {
7271
7377
  return { ja: value, en: value };
7272
7378
  }
@@ -7852,6 +7958,7 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7852
7958
  voiceStyle,
7853
7959
  voiceModel,
7854
7960
  model,
7961
+ voiceCatalogMetadata,
7855
7962
  emotionStyles,
7856
7963
  className,
7857
7964
  style,
@@ -8015,6 +8122,10 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
8015
8122
  const isProsodyPopover = insertion.tagName !== void 0 && PROSODY_POPOVER_TAGS.has(insertion.tagName);
8016
8123
  const isTextPopover = insertion.tagName !== void 0 && TEXT_POPOVER_TAGS.has(insertion.tagName);
8017
8124
  const insertionButtonStyle = insertion.tagName && activeTags.has(insertion.tagName.toLowerCase()) ? { ...toolbarButtonStyle, ...editorStyles.toolbarButtonActive } : toolbarButtonStyle;
8125
+ const activeVoiceName = popoverVoiceName ?? findFirstVoiceName(draftDocument.children ?? []);
8126
+ const activeVoice = voiceCatalog?.find((voice) => voice.name.toLowerCase() === activeVoiceName?.toLowerCase());
8127
+ const insertionDisabled = !isVoiceTagSupported(insertion.tagName, activeVoice);
8128
+ const disabledReason = insertionDisabled ? language === "ja" ? `\u9078\u629E\u4E2D\u306E\u97F3\u58F0\u306F <${insertion.tagName}> \u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\u3002` : `The selected voice does not support <${insertion.tagName}>.` : void 0;
8018
8129
  const props = {
8019
8130
  language,
8020
8131
  isDarkTheme,
@@ -8023,6 +8134,8 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
8023
8134
  toolbarButtonStyle: insertionButtonStyle,
8024
8135
  emptyOptionsMessage: copy.noAvailableOptions,
8025
8136
  isReadOnly,
8137
+ disabled: insertionDisabled,
8138
+ disabledReason,
8026
8139
  openPopoverId,
8027
8140
  menuPosition: popoverPosition,
8028
8141
  menuRef: setPopoverMenuRef,
@@ -8251,6 +8364,7 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
8251
8364
  voiceLocale,
8252
8365
  voiceRegion,
8253
8366
  voiceStyle,
8367
+ voiceCatalogMetadata,
8254
8368
  voiceModel,
8255
8369
  model
8256
8370
  }