@ssml-builder-js/ssml-editor-react 2.14.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 +11 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +84 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +84 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/SsmlEditor.tsx +8 -0
- package/src/components/VisualSsmlEditor.tsx +87 -2
- package/test/ui-components.test.tsx +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssml-builder-js/ssml-editor-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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.
|
|
55
|
+
"@ssml-builder-js/ssml-core": "^2.15.0"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/SsmlEditor.tsx
CHANGED
|
@@ -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>
|
|
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
|
|
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 });
|