@ssml-builder-js/ssml-editor-react 2.7.0 → 2.8.1

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.7.0",
3
+ "version": "2.8.1",
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.7.0"
55
+ "@ssml-builder-js/ssml-core": "^2.8.1"
56
56
  }
57
57
  }
@@ -6,6 +6,8 @@ import { isSsmlEditorButtonVisible, type SsmlEditorButton, type SsmlEditorButton
6
6
  import {
7
7
  BREAK_TIME_DESCRIPTIONS,
8
8
  BREAK_TIME_PRESETS,
9
+ AUDIO_DURATION_DESCRIPTIONS,
10
+ AUDIO_DURATION_PRESETS,
9
11
  EMPHASIS_LEVEL_DESCRIPTIONS,
10
12
  EMPHASIS_LEVEL_PRESETS,
11
13
  EXPRESS_AS_STYLE_DESCRIPTIONS,
@@ -39,7 +41,7 @@ import { useSsmlEditorState } from "./hooks/useSsmlEditorState";
39
41
  import { useSsmlMonaco } from "./hooks/useSsmlMonaco";
40
42
  import { editorStyles as styles } from "./styles/editorStyles";
41
43
  const UNGROUPED_TOOLBAR_GROUP = "__ssml-editor-ungrouped__";
42
- const TIMING_POPOVER_TAGS = new Set(["break", "mstts:silence"]);
44
+ const TIMING_POPOVER_TAGS = new Set(["break", "mstts:silence", "mstts:audioduration"]);
43
45
  const PROSODY_POPOVER_TAGS = new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
44
46
  const TEXT_POPOVER_TAGS = new Set(["sub", "say-as", "phoneme", "w", "lang"]);
45
47
 
@@ -365,13 +367,34 @@ export const SSML_INSERTIONS = [
365
367
  mode: "insert",
366
368
  }),
367
369
  },
370
+ {
371
+ id: "mstts:audioduration",
372
+ icon: "◷",
373
+ tagName: "mstts:audioduration",
374
+ selfClosing: true,
375
+ labels: { ja: "音声長", en: "Audio duration" },
376
+ descriptions: {
377
+ ja: "合成音声の目標時間を指定します。",
378
+ en: "Sets the target duration of synthesized audio.",
379
+ },
380
+ parameterDescription: {
381
+ ja: "目標時間を選択します。",
382
+ en: "Selects the target duration.",
383
+ },
384
+ options: createInsertionOptions(AUDIO_DURATION_PRESETS, AUDIO_DURATION_DESCRIPTIONS),
385
+ createTemplate: (value) => ({
386
+ prefix: `<mstts:audioduration value="${value}"/>`,
387
+ suffix: "",
388
+ mode: "insert",
389
+ }),
390
+ },
368
391
  ] satisfies readonly SsmlInsertionDefinition[];
369
392
 
370
393
  const DEFAULT_INSERTION_GROUPS = [
371
394
  {
372
395
  id: "pauses",
373
396
  labels: { ja: "間・無音", en: "Pauses" },
374
- insertionIds: ["break", "mstts:silence"],
397
+ insertionIds: ["break", "mstts:silence", "mstts:audioduration"],
375
398
  },
376
399
  {
377
400
  id: "prosody",
@@ -13,6 +13,7 @@ export type SsmlEditorInsertionButton =
13
13
  | "mark"
14
14
  | "bookmark"
15
15
  | "mstts:silence"
16
+ | "mstts:audioduration"
16
17
  | "mstts:viseme"
17
18
  | (string & {});
18
19
 
@@ -0,0 +1,94 @@
1
+ /** Generated by scripts/sync-azure-voices.ts. */
2
+ export const AZURE_VOICE_STYLE_MAP = {
3
+ "de-DE-ConradNeural": ["cheerful", "sad"],
4
+ "de-DE-KatjaNeural": ["cheerful", "sad"],
5
+ "en-US-AndrewNeural": ["empathetic", "relieved"],
6
+ "en-US-GuyNeural": [
7
+ "angry",
8
+ "cheerful",
9
+ "excited",
10
+ "friendly",
11
+ "hopeful",
12
+ "newscast",
13
+ "sad",
14
+ "shouting",
15
+ "terrified",
16
+ "unfriendly",
17
+ "whispering",
18
+ ],
19
+ "en-US-JennyMultilingualNeural": [
20
+ "cheerful",
21
+ "empathetic",
22
+ "excited",
23
+ "friendly",
24
+ "hopeful",
25
+ "sad",
26
+ "shouting",
27
+ "terrified",
28
+ "unfriendly",
29
+ "whispering",
30
+ ],
31
+ "en-US-JennyNeural": [
32
+ "assistant",
33
+ "chat",
34
+ "customerservice",
35
+ "newscast",
36
+ "cheerful",
37
+ "empathetic",
38
+ "excited",
39
+ "friendly",
40
+ "hopeful",
41
+ "sad",
42
+ "shouting",
43
+ "terrified",
44
+ "unfriendly",
45
+ "whispering",
46
+ ],
47
+ "es-ES-ElviraNeural": [],
48
+ "fil-PH-AngeloNeural": [],
49
+ "fr-FR-DeniseNeural": ["cheerful", "sad"],
50
+ "fr-FR-HenriNeural": ["cheerful", "sad"],
51
+ "id-ID-GadisNeural": [],
52
+ "it-IT-ElsaNeural": ["cheerful", "sad"],
53
+ "ja-JP-KeitaNeural": ["chat"],
54
+ "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
55
+ "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
56
+ "ko-KR-SunHiNeural": ["cheerful", "sad"],
57
+ "ms-MY-YasminNeural": [],
58
+ "pt-BR-FranciscaNeural": ["calm"],
59
+ "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
60
+ "th-TH-PremwadeeNeural": [],
61
+ "vi-VN-HoaiMyNeural": [],
62
+ "zh-CN-XiaoxiaoNeural": [
63
+ "assistant",
64
+ "chat",
65
+ "customerservice",
66
+ "newscast",
67
+ "cheerful",
68
+ "empathetic",
69
+ "excited",
70
+ "friendly",
71
+ "hopeful",
72
+ "sad",
73
+ "terrified",
74
+ "whispering",
75
+ "poetry-reading",
76
+ "sports_commentary",
77
+ "sports_commentary_excited",
78
+ "story",
79
+ ],
80
+ "zh-CN-YunxiNeural": [
81
+ "narration-relaxed",
82
+ "embarrassed",
83
+ "fearful",
84
+ "sad",
85
+ "disgruntled",
86
+ "serious",
87
+ "angry",
88
+ "depressed",
89
+ "chat",
90
+ "cheerful",
91
+ "assistant",
92
+ ],
93
+ "zh-TW-HsiaoChenNeural": [],
94
+ } as const satisfies Readonly<Record<string, readonly string[]>>;
@@ -1,5 +1,8 @@
1
1
  type SsmlPresetDescription = Readonly<Record<"ja" | "en", string>>;
2
2
 
3
+ // @ts-expect-error The Node strip-types test runner requires the explicit TypeScript extension.
4
+ import { AZURE_VOICE_STYLE_MAP } from "./azureVoiceStyleMap.generated.ts";
5
+
3
6
  export interface SsmlPreset {
4
7
  id: string;
5
8
  label: string;
@@ -119,7 +122,7 @@ export function getExpressAsStyleCategory(style: string): ExpressAsStyleCategory
119
122
  return "other";
120
123
  }
121
124
 
122
- export const VOICE_STYLE_MAP = {
125
+ const LEGACY_VOICE_STYLE_MAP = {
123
126
  "ja-JP-MayuNeural": ["calm", "cheerful", "sad"],
124
127
  "ja-JP-KeitaNeural": ["chat"],
125
128
  "ja-JP-NanamiNeural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
@@ -206,7 +209,11 @@ export const VOICE_STYLE_MAP = {
206
209
  "ru-RU-SvetlanaNeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"],
207
210
  } as const satisfies Readonly<Record<string, readonly ExpressAsStyle[]>>;
208
211
 
209
- const VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map<string, readonly ExpressAsStyle[]>(
212
+ /** Snapshot generated from Azure's List Voices API. The legacy map is only a safety fallback for an empty generated file. */
213
+ export const VOICE_STYLE_MAP =
214
+ Object.keys(AZURE_VOICE_STYLE_MAP).length > 0 ? AZURE_VOICE_STYLE_MAP : LEGACY_VOICE_STYLE_MAP;
215
+
216
+ const VOICE_STYLE_MAP_BY_NORMALIZED_NAME = new Map<string, readonly string[]>(
210
217
  Object.entries(VOICE_STYLE_MAP).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles]),
211
218
  );
212
219
 
@@ -254,6 +261,7 @@ export const SAY_AS_PRESETS = [
254
261
  ] as const;
255
262
  export const LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"] as const;
256
263
  export const SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"] as const;
264
+ export const AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"] as const;
257
265
  export const SILENCE_TYPE_PRESETS = [
258
266
  "Leading",
259
267
  "Tailing",
@@ -262,6 +270,11 @@ export const SILENCE_TYPE_PRESETS = [
262
270
  "Semicolon",
263
271
  "Enumerationcomma",
264
272
  ] as const;
273
+ export const AUDIO_DURATION_DESCRIPTIONS = {
274
+ "5s": { ja: "5秒", en: "5 seconds" },
275
+ "10s": { ja: "10秒", en: "10 seconds" },
276
+ "30s": { ja: "30秒", en: "30 seconds" },
277
+ } as const;
265
278
  export const PHONEME_ALPHABET_PRESETS = ["ipa", "sapi", "ups", "x-sampa"] as const;
266
279
  export const VISEME_TYPE_PRESETS = ["redlips_front", "FacialExpression"] as const;
267
280
 
@@ -306,6 +319,9 @@ export const SSML_ATTRIBUTE_PRESETS = {
306
319
  type: SILENCE_TYPE_PRESETS,
307
320
  value: SILENCE_VALUE_PRESETS,
308
321
  },
322
+ "mstts:audioduration": {
323
+ value: AUDIO_DURATION_PRESETS,
324
+ },
309
325
  silence: {
310
326
  type: SILENCE_TYPE_PRESETS,
311
327
  value: SILENCE_VALUE_PRESETS,
package/src/formatXml.ts CHANGED
@@ -7,6 +7,7 @@ export const INTRINSICALLY_EMPTY_ELEMENTS = new Set([
7
7
  "lexicon",
8
8
  "mark",
9
9
  "mstts:silence",
10
+ "mstts:audioduration",
10
11
  "mstts:viseme",
11
12
  "silence",
12
13
  "viseme",
@@ -24,7 +24,7 @@ import type { MonacoEditor, SsmlSyntaxError } from "../ssmlDiagnostics";
24
24
  import type { SsmlCodeLensAction } from "../ssmlCodeLens";
25
25
  import { SELECTION_OVERLAY_ABOVE_THRESHOLD_LINES } from "../constants/ui";
26
26
 
27
- const TIMING_INSERTION_TAGS = new Set(["break", "mstts:silence"]);
27
+ const TIMING_INSERTION_TAGS = new Set(["break", "mstts:silence", "mstts:audioduration"]);
28
28
  const PROSODY_INSERTION_TAGS = new Set(["prosody", "mstts:express-as", "voice", "emphasis"]);
29
29
  const TEXT_INSERTION_TAGS = new Set(["sub", "say-as", "phoneme", "w", "lang"]);
30
30
 
@@ -53,8 +53,8 @@ export interface UseSsmlEditorStateOptions {
53
53
  }
54
54
 
55
55
  interface PendingCodeLensAttribute {
56
- insertionId: "rate" | "pitch" | "break";
57
- attributeName: "rate" | "pitch" | "time";
56
+ insertionId: "rate" | "pitch" | "break" | "mstts:audioduration";
57
+ attributeName: "rate" | "pitch" | "time" | "value";
58
58
  tagRange: { start: number; end: number };
59
59
  modelVersionId: number;
60
60
  }
package/src/locales.ts CHANGED
@@ -273,6 +273,13 @@ export const SSML_HOVER_COPY: Readonly<Record<SsmlEditorLocale, SsmlHoverLocale>
273
273
  type: { title: "type", description: "ビゼームイベントの形式。" },
274
274
  },
275
275
  },
276
+ "mstts:audioduration": {
277
+ title: "音声長",
278
+ description: "合成音声の目標時間を指定します。",
279
+ parameters: {
280
+ value: { title: "value", description: "目標時間。例: `10s`、`5000ms`、`00:00:10`。" },
281
+ },
282
+ },
276
283
  },
277
284
  },
278
285
  en: {
@@ -430,6 +437,13 @@ export const SSML_HOVER_COPY: Readonly<Record<SsmlEditorLocale, SsmlHoverLocale>
430
437
  type: { title: "type", description: "The viseme event format." },
431
438
  },
432
439
  },
440
+ "mstts:audioduration": {
441
+ title: "Audio duration",
442
+ description: "Sets the target duration of synthesized audio.",
443
+ parameters: {
444
+ value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." },
445
+ },
446
+ },
433
447
  },
434
448
  },
435
449
  };
@@ -5,8 +5,8 @@ import type { SsmlTagRange } from "./ssmlContext";
5
5
  export type SsmlCodeLensAction =
6
6
  | {
7
7
  type: "attribute";
8
- insertionId: "rate" | "pitch" | "break";
9
- attributeName: "rate" | "pitch" | "time";
8
+ insertionId: "rate" | "pitch" | "break" | "mstts:audioduration";
9
+ attributeName: "rate" | "pitch" | "time" | "value";
10
10
  tagRange: SsmlTagRange;
11
11
  }
12
12
  | {
@@ -139,7 +139,7 @@ export function registerSsmlCodeLens(
139
139
  break;
140
140
  }
141
141
  const tag = source.slice(tagStart, tagEnd + 1);
142
- const tagName = tag.match(/^<\s*(prosody|break)\b/i)?.[1]?.toLowerCase();
142
+ const tagName = tag.match(/^<\s*(prosody|break|mstts:audioduration)\b/i)?.[1]?.toLowerCase();
143
143
  index = tagEnd + 1;
144
144
  if (!tagName) {
145
145
  continue;
@@ -171,7 +171,22 @@ export function registerSsmlCodeLens(
171
171
  elementRange,
172
172
  }),
173
173
  );
174
- } else if (/\/\s*>$/.test(tag)) {
174
+ } else if (tagName === "mstts:audioduration" && /\/\s*>$/.test(tag)) {
175
+ lenses.push(
176
+ createLens(
177
+ model,
178
+ tagStart,
179
+ tagEnd + 1,
180
+ `⚡ Duration: ${getAttributeValue(tag, "value") ?? "default"} (Click to edit)`,
181
+ { type: "attribute", insertionId: "mstts:audioduration", attributeName: "value", tagRange },
182
+ ),
183
+ createLens(model, tagStart, tagEnd + 1, "Delete", {
184
+ type: "delete",
185
+ tagRange,
186
+ elementRange,
187
+ }),
188
+ );
189
+ } else if (tagName === "break" && /\/\s*>$/.test(tag)) {
175
190
  lenses.push(
176
191
  createLens(
177
192
  model,
@@ -42,6 +42,10 @@ const SSML_COMPLETION_SNIPPETS = [
42
42
  label: "mstts:express-as",
43
43
  insertText: `<mstts:express-as style="cheerful">\${1:text}</mstts:express-as>`,
44
44
  },
45
+ {
46
+ label: "mstts:audioduration",
47
+ insertText: '<mstts:audioduration value="10s" />',
48
+ },
45
49
  {
46
50
  label: "sub",
47
51
  insertText: `<sub alias="\${1:読み}">\${2:漢字}</sub>`,
package/src/ssmlHover.ts CHANGED
@@ -328,6 +328,17 @@ const SSML_TAG_DEFINITIONS: readonly SsmlTagDefinition[] = [
328
328
  },
329
329
  ],
330
330
  },
331
+ {
332
+ name: "mstts:audioduration",
333
+ description: "Sets the target duration of synthesized audio.",
334
+ parameters: [
335
+ {
336
+ name: "value",
337
+ description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`.",
338
+ example: "10s",
339
+ },
340
+ ],
341
+ },
331
342
  ] as const;
332
343
 
333
344
  export { SSML_TAG_DEFINITIONS };
@@ -2,6 +2,8 @@ import type { SsmlEditorLocalizedText } from "./locales";
2
2
  import {
3
3
  BREAK_TIME_DESCRIPTIONS,
4
4
  BREAK_TIME_PRESETS,
5
+ AUDIO_DURATION_DESCRIPTIONS,
6
+ AUDIO_DURATION_PRESETS,
5
7
  EMPHASIS_LEVEL_DESCRIPTIONS,
6
8
  EMPHASIS_LEVEL_PRESETS,
7
9
  EXPRESS_AS_STYLE_DESCRIPTIONS,
@@ -320,13 +322,34 @@ export const SSML_INSERTIONS = [
320
322
  mode: "insert",
321
323
  }),
322
324
  },
325
+ {
326
+ id: "mstts:audioduration",
327
+ icon: "◷",
328
+ tagName: "mstts:audioduration",
329
+ selfClosing: true,
330
+ labels: { ja: "音声長", en: "Audio duration" },
331
+ descriptions: {
332
+ ja: "合成音声の目標時間を指定します。",
333
+ en: "Sets the target duration of synthesized audio.",
334
+ },
335
+ parameterDescription: {
336
+ ja: "目標時間を選択します。",
337
+ en: "Selects the target duration.",
338
+ },
339
+ options: createInsertionOptions(AUDIO_DURATION_PRESETS, AUDIO_DURATION_DESCRIPTIONS),
340
+ createTemplate: (value) => ({
341
+ prefix: `<mstts:audioduration value="${value}"/>`,
342
+ suffix: "",
343
+ mode: "insert",
344
+ }),
345
+ },
323
346
  ] satisfies readonly SsmlEditorInsertionDefinition[];
324
347
 
325
348
  export const DEFAULT_INSERTION_GROUPS = [
326
349
  {
327
350
  id: "pauses",
328
351
  labels: { ja: "間・無音", en: "Pauses" },
329
- insertionIds: ["break", "mstts:silence"],
352
+ insertionIds: ["break", "mstts:silence", "mstts:audioduration"],
330
353
  },
331
354
  {
332
355
  id: "prosody",
@@ -80,7 +80,23 @@ test("provides attribute values from the active SSML tag and attribute", () => {
80
80
  );
81
81
  });
82
82
 
83
- test("generates CodeLens actions for prosody and break tags", () => {
83
+ test("provides the audioduration snippet and value completions", () => {
84
+ const tagSuggestions = getSuggestions("<");
85
+ assert.equal(
86
+ tagSuggestions.some(
87
+ (suggestion) =>
88
+ suggestion.label === "mstts:audioduration" && suggestion.insertText === '<mstts:audioduration value="10s" />',
89
+ ),
90
+ true,
91
+ );
92
+ const valueSuggestions = getSuggestions('<mstts:audioduration value="');
93
+ assert.equal(
94
+ valueSuggestions.some((suggestion) => suggestion.label === "10s"),
95
+ true,
96
+ );
97
+ });
98
+
99
+ test("generates CodeLens actions for prosody, break, and audioduration tags", () => {
84
100
  let provider: Parameters<Monaco["languages"]["registerCodeLensProvider"]>[1] | undefined;
85
101
  let commandId: string | undefined;
86
102
  let commandHandler: ((accessor: unknown, ...args: unknown[]) => void) | undefined;
@@ -102,7 +118,8 @@ test("generates CodeLens actions for prosody and break tags", () => {
102
118
  const editor = {
103
119
  addAction: () => ({ dispose() {} }),
104
120
  } as never;
105
- const source = '<prosody rate="+10%" pitch="high">Hello</prosody><break time="500ms"/>';
121
+ const source =
122
+ '<prosody rate="+10%" pitch="high">Hello</prosody><break time="500ms"/><mstts:audioduration value="10s"/>';
106
123
  registerSsmlCodeLens(monaco, editor, () => undefined);
107
124
  const model = {
108
125
  getValue: () => source,
@@ -118,6 +135,8 @@ test("generates CodeLens actions for prosody and break tags", () => {
118
135
  "Unwrap",
119
136
  "⚡ Time: 500ms (Click to edit)",
120
137
  "Delete",
138
+ "⚡ Duration: 10s (Click to edit)",
139
+ "Delete",
121
140
  ],
122
141
  );
123
142
  assert.equal(commandId, "ssml-editor.codeLens");