@ssml-builder-js/ssml-editor-react 2.11.0 → 2.13.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/src/locales.ts CHANGED
@@ -60,6 +60,160 @@ export interface SsmlHoverLocale {
60
60
  tags: Readonly<Record<string, SsmlHoverTagCopy>>;
61
61
  }
62
62
 
63
+ export interface SsmlElementLocaleCopy {
64
+ label: string;
65
+ description: string;
66
+ validationErrors: Readonly<Record<string, string>>;
67
+ }
68
+
69
+ /** Localized labels and validation copy for Azure extension elements. */
70
+ export const SSML_ELEMENT_COPY: Readonly<Record<SsmlEditorLocale, Readonly<Record<string, SsmlElementLocaleCopy>>>> = {
71
+ ja: {
72
+ dialog: {
73
+ label: "ダイアログ",
74
+ description: "複数の話者ターンをまとめます。",
75
+ validationErrors: { turn: "turn は dialog の直下に配置してください。" },
76
+ },
77
+ "mstts:dialog": {
78
+ label: "ダイアログ",
79
+ description: "複数の話者ターンをまとめます。",
80
+ validationErrors: { turn: "turn は dialog の直下に配置してください。" },
81
+ },
82
+ turn: {
83
+ label: "話者ターン",
84
+ description: "ダイアログ内の話者と発話内容を指定します。",
85
+ validationErrors: { voice: "voice または speaker を指定してください。" },
86
+ },
87
+ "mstts:turn": {
88
+ label: "話者ターン",
89
+ description: "ダイアログ内の話者と発話内容を指定します。",
90
+ validationErrors: { voice: "voice または speaker を指定してください。" },
91
+ },
92
+ backgroundaudio: {
93
+ label: "背景音声",
94
+ description: "合成音声の背後で再生する音声ファイルを指定します。",
95
+ validationErrors: { src: "背景音声には URL を指定してください。" },
96
+ },
97
+ "mstts:backgroundaudio": {
98
+ label: "背景音声",
99
+ description: "合成音声の背後で再生する音声ファイルを指定します。",
100
+ validationErrors: { src: "背景音声には URL を指定してください。" },
101
+ },
102
+ ttsembedding: {
103
+ label: "音声埋め込み",
104
+ description: "カスタム音声または話者プロファイルのメタデータを埋め込みます。",
105
+ validationErrors: { speakerProfileId: "話者プロファイル ID を指定してください。" },
106
+ },
107
+ "mstts:ttsembedding": {
108
+ label: "音声埋め込み",
109
+ description: "カスタム音声または話者プロファイルのメタデータを埋め込みます。",
110
+ validationErrors: { speakerProfileId: "話者プロファイル ID を指定してください。" },
111
+ },
112
+ embedding: {
113
+ label: "埋め込み",
114
+ description: "話者埋め込みのメタデータを指定します。",
115
+ validationErrors: {},
116
+ },
117
+ "mstts:embedding": {
118
+ label: "埋め込み",
119
+ description: "話者埋め込みのメタデータを指定します。",
120
+ validationErrors: {},
121
+ },
122
+ silence: {
123
+ label: "無音",
124
+ description: "発話前後または句読点の境界に無音を追加します。",
125
+ validationErrors: { value: "無音の長さを指定してください。" },
126
+ },
127
+ "mstts:silence": {
128
+ label: "無音",
129
+ description: "発話前後または句読点の境界に無音を追加します。",
130
+ validationErrors: { value: "無音の長さを指定してください。" },
131
+ },
132
+ voiceconversion: {
133
+ label: "音声変換",
134
+ description: "カスタム音声の変換メタデータを指定します。",
135
+ validationErrors: {},
136
+ },
137
+ "mstts:voiceconversion": {
138
+ label: "音声変換",
139
+ description: "カスタム音声の変換メタデータを指定します。",
140
+ validationErrors: {},
141
+ },
142
+ },
143
+ en: {
144
+ dialog: {
145
+ label: "Dialog",
146
+ description: "Groups multiple speaker turns.",
147
+ validationErrors: { turn: "Place turns directly inside a dialog." },
148
+ },
149
+ "mstts:dialog": {
150
+ label: "Dialog",
151
+ description: "Groups multiple speaker turns.",
152
+ validationErrors: { turn: "Place turns directly inside a dialog." },
153
+ },
154
+ turn: {
155
+ label: "Dialog turn",
156
+ description: "Specifies a speaker and utterance within a dialog.",
157
+ validationErrors: { voice: "Provide a voice or speaker." },
158
+ },
159
+ "mstts:turn": {
160
+ label: "Dialog turn",
161
+ description: "Specifies a speaker and utterance within a dialog.",
162
+ validationErrors: { voice: "Provide a voice or speaker." },
163
+ },
164
+ backgroundaudio: {
165
+ label: "Background audio",
166
+ description: "Specifies an audio file played behind synthesized speech.",
167
+ validationErrors: { src: "Provide a background audio URL." },
168
+ },
169
+ "mstts:backgroundaudio": {
170
+ label: "Background audio",
171
+ description: "Specifies an audio file played behind synthesized speech.",
172
+ validationErrors: { src: "Provide a background audio URL." },
173
+ },
174
+ ttsembedding: {
175
+ label: "TTS embedding",
176
+ description: "Embeds custom voice or speaker profile metadata.",
177
+ validationErrors: { speakerProfileId: "Provide a speaker profile ID." },
178
+ },
179
+ "mstts:ttsembedding": {
180
+ label: "TTS embedding",
181
+ description: "Embeds custom voice or speaker profile metadata.",
182
+ validationErrors: { speakerProfileId: "Provide a speaker profile ID." },
183
+ },
184
+ embedding: {
185
+ label: "Embedding",
186
+ description: "Specifies speaker embedding metadata.",
187
+ validationErrors: {},
188
+ },
189
+ "mstts:embedding": {
190
+ label: "Embedding",
191
+ description: "Specifies speaker embedding metadata.",
192
+ validationErrors: {},
193
+ },
194
+ silence: {
195
+ label: "Silence",
196
+ description: "Adds silence before, after, or around punctuation.",
197
+ validationErrors: { value: "Provide a silence duration." },
198
+ },
199
+ "mstts:silence": {
200
+ label: "Silence",
201
+ description: "Adds silence before, after, or around punctuation.",
202
+ validationErrors: { value: "Provide a silence duration." },
203
+ },
204
+ voiceconversion: {
205
+ label: "Voice conversion",
206
+ description: "Specifies custom voice conversion metadata.",
207
+ validationErrors: {},
208
+ },
209
+ "mstts:voiceconversion": {
210
+ label: "Voice conversion",
211
+ description: "Specifies custom voice conversion metadata.",
212
+ validationErrors: {},
213
+ },
214
+ },
215
+ };
216
+
63
217
  export const EDITOR_COPY: Readonly<Record<SsmlEditorLocale, EditorCopy>> = {
64
218
  ja: {
65
219
  editorAriaLabel: "SSMLエディター",
@@ -280,6 +434,42 @@ export const SSML_HOVER_COPY: Readonly<Record<SsmlEditorLocale, SsmlHoverLocale>
280
434
  value: { title: "value", description: "目標時間。例: `10s`、`5000ms`、`00:00:10`。" },
281
435
  },
282
436
  },
437
+ "mstts:dialog": { title: "ダイアログ", description: "複数の話者ターンをまとめます。", parameters: {} },
438
+ "mstts:turn": {
439
+ title: "話者ターン",
440
+ description: "ダイアログ内の話者と発話内容を指定します。",
441
+ parameters: {
442
+ voice: { title: "voice", description: "このターンで使用する Azure 音声名。" },
443
+ speaker: { title: "speaker", description: "話者識別子。" },
444
+ },
445
+ },
446
+ "mstts:backgroundaudio": {
447
+ title: "背景音声",
448
+ description: "合成音声の背後で再生する音声ファイルを指定します。",
449
+ parameters: { src: { title: "src", description: "音声ファイルの HTTPS URL。" } },
450
+ },
451
+ "mstts:ttsembedding": {
452
+ title: "音声埋め込み",
453
+ description: "カスタム音声または話者プロファイルのメタデータを埋め込みます。",
454
+ parameters: { speakerProfileId: { title: "speakerProfileId", description: "話者プロファイル ID。" } },
455
+ },
456
+ "mstts:embedding": {
457
+ title: "埋め込み",
458
+ description: "話者埋め込みのメタデータを指定します。",
459
+ parameters: {
460
+ id: { title: "id", description: "埋め込み ID。" },
461
+ speakerProfileId: { title: "speakerProfileId", description: "話者プロファイル ID。" },
462
+ },
463
+ },
464
+ "mstts:voiceconversion": {
465
+ title: "音声変換",
466
+ description: "カスタム音声の変換メタデータを指定します。",
467
+ parameters: {
468
+ url: { title: "url", description: "音声変換プロファイル URL。" },
469
+ profile: { title: "profile", description: "変換プロファイル。" },
470
+ speakerProfileId: { title: "speakerProfileId", description: "話者プロファイル ID。" },
471
+ },
472
+ },
283
473
  },
284
474
  },
285
475
  en: {
@@ -444,6 +634,42 @@ export const SSML_HOVER_COPY: Readonly<Record<SsmlEditorLocale, SsmlHoverLocale>
444
634
  value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." },
445
635
  },
446
636
  },
637
+ "mstts:dialog": { title: "Dialog", description: "Groups multiple speaker turns.", parameters: {} },
638
+ "mstts:turn": {
639
+ title: "Dialog turn",
640
+ description: "Specifies a speaker and utterance within a dialog.",
641
+ parameters: {
642
+ voice: { title: "voice", description: "The Azure voice used by this turn." },
643
+ speaker: { title: "speaker", description: "The speaker identifier." },
644
+ },
645
+ },
646
+ "mstts:backgroundaudio": {
647
+ title: "Background audio",
648
+ description: "Specifies an audio file played behind synthesized speech.",
649
+ parameters: { src: { title: "src", description: "The HTTPS URL of the audio file." } },
650
+ },
651
+ "mstts:ttsembedding": {
652
+ title: "TTS embedding",
653
+ description: "Embeds custom voice or speaker profile metadata.",
654
+ parameters: { speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." } },
655
+ },
656
+ "mstts:embedding": {
657
+ title: "Embedding",
658
+ description: "Specifies speaker embedding metadata.",
659
+ parameters: {
660
+ id: { title: "id", description: "The embedding ID." },
661
+ speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." },
662
+ },
663
+ },
664
+ "mstts:voiceconversion": {
665
+ title: "Voice conversion",
666
+ description: "Specifies custom voice conversion metadata.",
667
+ parameters: {
668
+ url: { title: "url", description: "The voice conversion profile URL." },
669
+ profile: { title: "profile", description: "The conversion profile." },
670
+ speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." },
671
+ },
672
+ },
447
673
  },
448
674
  },
449
675
  };
@@ -621,6 +621,84 @@ describe("SsmlEditor props", () => {
621
621
  );
622
622
  });
623
623
 
624
+ it("provides forms for every supported Azure element", async () => {
625
+ const user = userEvent.setup();
626
+ const onChange = vi.fn();
627
+ renderEditor({
628
+ editMode: "visual",
629
+ onChange,
630
+ document: {
631
+ ...editorDocument,
632
+ children: [
633
+ { type: "voice", name: "en-US-JennyNeural", children: ["Hello"] },
634
+ { type: "prosody", rate: "slow", children: ["Hello"] },
635
+ { type: "say-as", interpretAs: "cardinal", children: ["1"] },
636
+ { type: "phoneme", alphabet: "ipa", ph: "həˈloʊ", children: ["Hello"] },
637
+ { type: "audio", src: "https://allowed.test/a.mp3" },
638
+ { type: "mark", name: "chapter-1" },
639
+ { type: "bookmark", mark: "chapter-1" },
640
+ { type: "mstts:silence", typeValue: "Comma", value: "100ms" },
641
+ { type: "mstts:audioduration", value: "10s" },
642
+ { type: "mstts:embedding", id: "speaker-1" },
643
+ { type: "mstts:voiceconversion", url: "https://allowed.test/profile" },
644
+ ],
645
+ },
646
+ });
647
+
648
+ const tree = screen.getByRole("navigation", { name: "SSML structure tree" });
649
+ const expectedFields = [
650
+ ["<voice>", "Voice name"],
651
+ ["<prosody>", "Rate"],
652
+ ["<say-as>", "Interpret as"],
653
+ ["<phoneme>", "Alphabet"],
654
+ ["<audio>", "Source URL"],
655
+ ["<mark>", "Mark name"],
656
+ ["<bookmark>", "Bookmark name"],
657
+ ["<mstts:silence>", "Silence type"],
658
+ ["<mstts:audioduration>", "Duration"],
659
+ ["<mstts:embedding>", "Embedding ID"],
660
+ ["<mstts:voiceconversion>", "Source URL"],
661
+ ];
662
+
663
+ for (const [element, field] of expectedFields) {
664
+ await user.click(within(tree).getByRole("button", { name: element }));
665
+ expect(screen.getByLabelText(field)).toBeTruthy();
666
+ }
667
+
668
+ await user.click(within(tree).getByRole("button", { name: "<mstts:embedding>" }));
669
+ await user.clear(screen.getByLabelText("Embedding ID"));
670
+ await user.type(screen.getByLabelText("Embedding ID"), "speaker-2");
671
+ expect(onChange).toHaveBeenLastCalledWith(
672
+ expect.objectContaining({
673
+ children: expect.arrayContaining([expect.objectContaining({ type: "mstts:embedding", id: "speaker-2" })]),
674
+ }),
675
+ );
676
+ });
677
+
678
+ it("retains voice and prosody context for visual selection preview", async () => {
679
+ const user = userEvent.setup();
680
+ const onPreviewSelection = vi.fn();
681
+ renderEditor({
682
+ editMode: "visual",
683
+ onPreviewSelection,
684
+ document: {
685
+ ...editorDocument,
686
+ children: [
687
+ {
688
+ type: "voice",
689
+ name: "en-US-JennyNeural",
690
+ children: [{ type: "prosody", rate: "slow", children: ["Hello world"] }],
691
+ },
692
+ ],
693
+ },
694
+ });
695
+
696
+ await user.click(screen.getByRole("button", { name: "Preview selection" }));
697
+ expect(onPreviewSelection).toHaveBeenCalledWith(
698
+ '<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US"><voice name="en-US-JennyNeural"><prosody rate="slow">Hello world</prosody></voice></speak>',
699
+ );
700
+ });
701
+
624
702
  it("edits Azure background audio settings in the visual editor", async () => {
625
703
  const user = userEvent.setup();
626
704
  const onChange = vi.fn();