@ssml-builder-js/ssml-editor-react 2.14.0 → 2.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssml-builder-js/ssml-editor-react",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "React-based SSML editor component",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,6 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@ssml-builder-js/ssml-core": "^2.14.0"
55
+ "@ssml-builder-js/ssml-core": "^2.16.0"
56
56
  }
57
57
  }
@@ -781,6 +781,10 @@ export interface SsmlEditorProps {
781
781
  voiceRegion?: string;
782
782
  /** Optional style filter for the visual voice selector. */
783
783
  voiceStyle?: string;
784
+ /** Selected Azure voice model used for live capability validation. */
785
+ voiceModel?: string;
786
+ /** Alias for voiceModel. */
787
+ model?: string;
784
788
  /** Candidate style values shown by the built-in emotion insertion. */
785
789
  emotionStyles?: readonly string[];
786
790
  /** Class name applied to the editor container. */
@@ -888,6 +892,8 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
888
892
  voiceLocale,
889
893
  voiceRegion,
890
894
  voiceStyle,
895
+ voiceModel,
896
+ model,
891
897
  emotionStyles,
892
898
  className,
893
899
  style,
@@ -1377,6 +1383,8 @@ export const SsmlEditor = forwardRef<SsmlEditorRef, SsmlEditorProps>(function Ss
1377
1383
  voiceLocale={voiceLocale}
1378
1384
  voiceRegion={voiceRegion}
1379
1385
  voiceStyle={voiceStyle}
1386
+ voiceModel={voiceModel}
1387
+ model={model}
1380
1388
  />
1381
1389
  </div>
1382
1390
  ) : (
@@ -6,6 +6,7 @@ import {
6
6
  type SsmlDocument,
7
7
  type SsmlElement,
8
8
  type SsmlNode,
9
+ type SsmlDiagnostic,
9
10
  } from "@ssml-builder-js/ssml-core";
10
11
  import { SSML_ELEMENT_COPY, type SsmlEditorLocale } from "../locales";
11
12
 
@@ -17,6 +18,7 @@ export interface VisualVoiceCatalogEntry {
17
18
  styles?: readonly string[];
18
19
  supportedTags?: readonly string[];
19
20
  unsupportedTags?: readonly string[];
21
+ models?: readonly string[];
20
22
  preview?: boolean;
21
23
  status?: "ga" | "preview" | "deprecated";
22
24
  }
@@ -53,6 +55,10 @@ export interface VisualSsmlEditorProps {
53
55
  voiceLocale?: string;
54
56
  voiceRegion?: string;
55
57
  voiceStyle?: string;
58
+ /** Selected Azure voice model used for live capability validation. */
59
+ voiceModel?: string;
60
+ /** Alias for voiceModel. */
61
+ model?: string;
56
62
  }
57
63
 
58
64
  interface TextLeaf {
@@ -294,6 +300,51 @@ function VoiceCapabilityMatrix({
294
300
  );
295
301
  }
296
302
 
303
+ function diagnosticMatchesElement(diagnostic: SsmlDiagnostic, element: SsmlElement): boolean {
304
+ const name = elementLabel(element).toLowerCase().replace("expressas", "mstts:express-as");
305
+ if (diagnostic.code === "azure-unsupported-style") return name === "mstts:express-as" || name === "express-as";
306
+ if (diagnostic.code === "azure-unsupported-tag-for-voice") {
307
+ return (
308
+ diagnostic.message.toLowerCase().includes(`<${name}>`) ||
309
+ diagnostic.message.toLowerCase().includes(`<${name.replace("mstts:", "")}>`)
310
+ );
311
+ }
312
+ if (diagnostic.code === "azure-locale-mismatch" || diagnostic.code === "azure-unsupported-model-for-voice")
313
+ return name === "voice" || name === "mstts:turn";
314
+ return false;
315
+ }
316
+
317
+ function elementWarnings(diagnostics: readonly SsmlDiagnostic[], element: SsmlElement): readonly SsmlDiagnostic[] {
318
+ return diagnostics.filter((diagnostic) => diagnosticMatchesElement(diagnostic, element));
319
+ }
320
+
321
+ function fieldWarnings(
322
+ diagnostics: readonly SsmlDiagnostic[],
323
+ element: SsmlElement,
324
+ field: VisualElementField,
325
+ ): readonly SsmlDiagnostic[] {
326
+ return elementWarnings(diagnostics, element).filter((diagnostic) => {
327
+ if (diagnostic.code === "azure-unsupported-style") return field.key === "style";
328
+ return field.key === "name" || field.key === "voice";
329
+ });
330
+ }
331
+
332
+ function WarningBadge({ messages }: { messages: readonly SsmlDiagnostic[] }): ReactElement | null {
333
+ if (messages.length === 0) return null;
334
+ return (
335
+ <span
336
+ role="status"
337
+ aria-label={messages.map((message) => message.message).join(" ")}
338
+ title={messages.map((message) => message.message).join(" ")}
339
+ data-ssml-warning-badge=""
340
+ data-testid="ssml-editor-warning-badge"
341
+ className="ssml-editor-warning-badge"
342
+ >
343
+
344
+ </span>
345
+ );
346
+ }
347
+
297
348
  function DefaultVoiceSelector({
298
349
  value,
299
350
  voices,
@@ -336,6 +387,7 @@ function VisualElementInspector({
336
387
  voiceLocale,
337
388
  voiceRegion,
338
389
  voiceStyle,
390
+ diagnostics,
339
391
  }: {
340
392
  document: SsmlDocument;
341
393
  element: SsmlElement;
@@ -349,6 +401,7 @@ function VisualElementInspector({
349
401
  voiceLocale?: string;
350
402
  voiceRegion?: string;
351
403
  voiceStyle?: string;
404
+ diagnostics: readonly SsmlDiagnostic[];
352
405
  }): ReactElement {
353
406
  if (customInspector) {
354
407
  return <>{customInspector({ document, element, path, readOnly, onChange: commit, locale })}</>;
@@ -361,7 +414,9 @@ function VisualElementInspector({
361
414
  const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
362
415
  return (
363
416
  <fieldset>
364
- <legend>{`<${elementLabel(element)}>`}</legend>
417
+ <legend>
418
+ {`<${elementLabel(element)}>`} <WarningBadge messages={elementWarnings(diagnostics, element)} />
419
+ </legend>
365
420
  {fields.length === 0 ? (
366
421
  <p>This element is preserved in the visual tree. Edit its attributes in Code mode.</p>
367
422
  ) : (
@@ -409,6 +464,7 @@ function VisualElementInspector({
409
464
  } as Record<string, string>
410
465
  )[field.key] ?? field.label)
411
466
  : field.label}
467
+ <WarningBadge messages={fieldWarnings(diagnostics, element, field)} />
412
468
  {field.key === "name" && elementLabel(element) === "voice" && voiceCatalog ? (
413
469
  renderSelector({
414
470
  value: inputValue,
@@ -509,11 +565,13 @@ function TreeNode({
509
565
  path,
510
566
  selectedPath,
511
567
  onSelect,
568
+ getWarnings,
512
569
  }: {
513
570
  node: SsmlNode;
514
571
  path: number[];
515
572
  selectedPath: number[] | null;
516
573
  onSelect: (path: number[]) => void;
574
+ getWarnings: (element: SsmlElement) => readonly SsmlDiagnostic[];
517
575
  }): ReactElement | null {
518
576
  if (!isElement(node)) return null;
519
577
  const name = elementLabel(node);
@@ -522,6 +580,7 @@ function TreeNode({
522
580
  <li>
523
581
  <button type="button" aria-current={isSelected ? "true" : undefined} onClick={() => onSelect(path)}>
524
582
  {`<${name}>`}
583
+ <WarningBadge messages={getWarnings(node)} />
525
584
  </button>
526
585
  {(node.children ?? []).length > 0 && (
527
586
  <ul>
@@ -532,6 +591,7 @@ function TreeNode({
532
591
  path={[...path, index]}
533
592
  selectedPath={selectedPath}
534
593
  onSelect={onSelect}
594
+ getWarnings={getWarnings}
535
595
  />
536
596
  ))}
537
597
  </ul>
@@ -552,6 +612,8 @@ export function VisualSsmlEditor({
552
612
  voiceLocale,
553
613
  voiceRegion,
554
614
  voiceStyle,
615
+ voiceModel,
616
+ model,
555
617
  }: VisualSsmlEditorProps): ReactElement {
556
618
  const [selectedPath, setSelectedPath] = useState<number[] | null>(null);
557
619
  const [selection, setSelection] = useState({ start: 0, end: 0 });
@@ -562,7 +624,27 @@ export function VisualSsmlEditor({
562
624
  selectedElement && isElement(selectedElement)
563
625
  ? (customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type])
564
626
  : undefined;
565
- const diagnostics = useMemo(() => validateAzureSsml(buildSsml(document)), [document]);
627
+ const validationOptions = useMemo(
628
+ () => ({
629
+ model: voiceModel ?? model,
630
+ customVoiceDefinitions: voiceCatalog?.map((voice) => ({
631
+ name: voice.name,
632
+ locale: voice.locale,
633
+ regions: voice.regions ?? (voice.region ? [voice.region] : undefined),
634
+ styles: voice.styles,
635
+ supportedTags: voice.supportedTags,
636
+ unsupportedTags: voice.unsupportedTags,
637
+ models: voice.models,
638
+ status: voice.status ?? (voice.preview ? "preview" : "ga"),
639
+ })),
640
+ }),
641
+ [model, voiceCatalog, voiceModel],
642
+ );
643
+ const diagnostics = useMemo(
644
+ () => validateAzureSsml(buildSsml(document), validationOptions) as SsmlDiagnostic[],
645
+ [document, validationOptions],
646
+ );
647
+ const getWarnings = (element: SsmlElement): readonly SsmlDiagnostic[] => elementWarnings(diagnostics, element);
566
648
  const commit = (nextDocument: SsmlDocument): void => onChange?.(nextDocument);
567
649
 
568
650
  const applyWrapper = (tag: SsmlElement["type"], attributes: Record<string, string>): void => {
@@ -604,6 +686,7 @@ export function VisualSsmlEditor({
604
686
  path={[index]}
605
687
  selectedPath={selectedPath}
606
688
  onSelect={setSelectedPath}
689
+ getWarnings={getWarnings}
607
690
  />
608
691
  ))}
609
692
  </ul>
@@ -635,6 +718,7 @@ export function VisualSsmlEditor({
635
718
  <legend>{localizedElementLabel(selectedElement, locale)}</legend>
636
719
  <label htmlFor="ssml-visual-turn-voice">
637
720
  {locale === "ja" ? "音声" : "Voice"}
721
+ <WarningBadge messages={elementWarnings(diagnostics, selectedElement)} />
638
722
  {voiceCatalog ? (
639
723
  (renderVoiceSelector ?? DefaultVoiceSelector)({
640
724
  value: selectedElement.voice ?? "",
@@ -744,6 +828,7 @@ export function VisualSsmlEditor({
744
828
  voiceLocale={voiceLocale}
745
829
  voiceRegion={voiceRegion}
746
830
  voiceStyle={voiceStyle}
831
+ diagnostics={diagnostics}
747
832
  />
748
833
  ) : selectedLeaf ? (
749
834
  <>
@@ -724,6 +724,34 @@ describe("SsmlEditor props", () => {
724
724
  );
725
725
  });
726
726
 
727
+ it("shows live capability warning badges for model, locale, and style mismatches", () => {
728
+ renderEditor({
729
+ editMode: "visual",
730
+ model: "neural-v2",
731
+ document: {
732
+ ...editorDocument,
733
+ children: [
734
+ {
735
+ type: "voice",
736
+ name: "en-US-ExampleNeural",
737
+ children: [{ type: "mstts:express-as", style: "sad", children: ["Hello"] }],
738
+ },
739
+ ],
740
+ },
741
+ voiceCatalog: [
742
+ {
743
+ name: "en-US-ExampleNeural",
744
+ locale: "ja-JP",
745
+ styles: ["cheerful"],
746
+ models: ["neural-v1"],
747
+ supportedTags: ["mstts:express-as"],
748
+ },
749
+ ],
750
+ });
751
+
752
+ expect(screen.getAllByTestId("ssml-editor-warning-badge").length).toBeGreaterThan(0);
753
+ });
754
+
727
755
  it("updates toolbar and popover text when the locale changes", async () => {
728
756
  const user = userEvent.setup();
729
757
  const { rerender } = renderEditor({ locale: "ja", showToolbarLabels: true });