ssml-builder-js 2.9.0 → 2.11.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/README.md CHANGED
@@ -91,7 +91,7 @@ Pages Router と App Router のどちらでも、Monaco を `next/dynamic` の `
91
91
  "use client";
92
92
  import dynamic from "next/dynamic";
93
93
  const SsmlEditor = dynamic(
94
- () => import("@ssml-builder-js/ssml-editor-react").then(({ SsmlEditor }) => SsmlEditor),
94
+ () => import("ssml-builder-js/react").then(({ SsmlEditor }) => SsmlEditor),
95
95
  { ssr: false },
96
96
  );
97
97
 
@@ -179,6 +179,8 @@ SSML の検証は、構文、Azure 固有の静的な意味、実サービスの
179
179
 
180
180
  `voice`、`prosody`、`break`、`express-as`、`say-as`、`phoneme`、`audio`、`lang`、`mark` に加えて、`mstts:dialog`、`mstts:turn`、`mstts:backgroundaudio`、`mstts:ttsembedding`、`mstts:embedding`、`mstts:voiceconversion` を型付きで表現できます。`type: "custom"` と `name` を指定すれば、未定義の XML 要素や追加属性も扱えます。`mstts:` 要素を含むドキュメントを生成すると、必要な Azure Speech 名前空間が自動的に追加されます。
181
181
 
182
+ `mstts:turn` は `voice` またはマルチトーカー用の `speaker` を指定でき、`mstts:ttsembedding` は `speakerProfileId`、`mstts:embedding` は `id`、`mstts:voiceconversion` は `url` と `profile` を専用プロパティで指定できます。`mstts:backgroundaudio` は `<speak>` 直下の先頭要素として 1 文書に 1 つだけ配置し、`fadein`/`fadeout` は単位なしの生ミリ秒(0〜10000)で指定します。
183
+
182
184
  `validateAzureSsml` は Azure Speech へ送信する前に実行する事前静的チェックです。返却される診断の `source` はパッケージ側の静的解析結果であることを示し、Azure Speech サービス側でのランタイム生成結果や実際の合成可否を表すものではありません。
183
185
 
184
186
  翻訳などで本文だけを置き換える場合は、`mapSsmlTextNodes` に変換関数を渡します。変換関数には直近の親タグと祖先タグの `path` が渡され、戻り値は `string` または `Promise<string>` を指定できます。`validateAzureSsml` は音声、属性値、音声スタイル、文字数、`audio` URL/オリジンを検証します。
@@ -206,7 +208,7 @@ const diagnostics = validateAzureSsml(ssml, {
206
208
  });
207
209
  ```
208
210
 
209
- 音声カタログは `AzureVoiceDefinition` の `name`、`locale`、`secondaryLocales`、`styles`、`supportedTags`、`unsupportedTags`、`models` で表現できます。`voiceDefinitions`(または `voiceCatalog`)を渡すと、組み込み台帳を外部定義で補完・上書きできます。`supportedTags`/`unsupportedTags` による音声別制約違反は `azure-unsupported-tag-for-voice` としてエラーになります。組み込み台帳にない音声は `azure-unknown-voice`(`unknownVoicePolicy` に従う)、登録済み音声への非対応スタイルは `azure-unsupported-style`、ロケール不一致は `azure-locale-mismatch` として区別されます。
211
+ 音声カタログは `AzureVoiceDefinition` の `name`、`locale`、`secondaryLocales`、`styles`、`supportedTags`、`unsupportedTags`、`models`、`regions`、`status` で表現できます。`voiceDefinitions`(または `voiceCatalog`)を渡すと、組み込み台帳を外部定義で補完・上書きできます。`supportedTags`/`unsupportedTags` による音声別制約違反は `azure-unsupported-tag-for-voice` としてエラーになります。組み込み台帳にない音声は `azure-unknown-voice`(`unknownVoicePolicy` に従う)、登録済み音声への非対応スタイルは `azure-unsupported-style`、ロケール不一致は `azure-locale-mismatch` として区別されます。
210
212
 
211
213
  `npm run sync:voices -- --regions eastus,japaneast` は Azure List Voices API をリージョンごとに取得し、重複を除いた TypeScript 音声定義と `azureVoiceCatalog.json`(生成日時、API バージョン、リージョン、収録数)を更新します。認証情報は `AZURE_SPEECH_KEY` と `AZURE_SPEECH_REGION(S)`、または CLI オプションで指定します。
212
214
 
@@ -245,6 +247,24 @@ Azure Speech は `<audio>` の URL を取得するため、任意の URL をそ
245
247
 
246
248
  `editMode="visual"` を指定するかツールバーの **Visual** を選ぶと、XML を直接見ずに構造ツリーとパンくずから親要素を選択し、フォームで本文の変更、rate、pitch、emotion、pause、pronunciation の適用ができます。Azure の配置違反や属性エラーはビジュアル領域にも表示されます。`editMode="code"` で Monaco に戻ります。
247
249
 
250
+ Visual Editor と Code Editor の対応要素は次のとおりです。
251
+
252
+ | 要素 | Visual Editor | Code Editor |
253
+ | --- | --- | --- |
254
+ | `voice`、`prosody`、`break`、`express-as`、`say-as`、`phoneme` | フォーム操作・構造ツリー | 完全対応 |
255
+ | `mstts:dialog` / `mstts:turn` | 話者ターンの追加・音声/話者/本文編集 | 完全対応・補完あり |
256
+ | `mstts:backgroundaudio` | URL、音量、フェードイン/アウト編集 | 完全対応・補完あり |
257
+ | `mstts:ttsembedding`、`mstts:embedding`、`mstts:voiceconversion` | 構造ツリーで保持 | 完全対応 |
258
+ | 未定義の XML 要素 | 構造ツリーで保持 | `custom` として編集 |
259
+
260
+ Azure Speech のライフサイクル対応状況は次のとおりです。GA 要素は通常の静的検証対象、プレビュー要素・音声は Warning、非推奨要素・音声は Info の診断を返します。`AzureVoiceDefinition.status` と `AzureValidationOptions.tagStatuses` で外部カタログの状態も指定できます。
261
+
262
+ | 状態 | 診断 | 対象 |
263
+ | --- | --- | --- |
264
+ | GA | なし | 標準 SSML、`mstts:dialog`、`mstts:backgroundaudio` |
265
+ | Preview | Warning | `mstts:voiceconversion`、`previewTags` / `tagStatuses` に指定した要素、`status: "preview"` の音声 |
266
+ | Deprecated | Info | `deprecatedTags` / `tagStatuses`、`status: "deprecated"` の音声 |
267
+
248
268
  `<mstts:express-as>` の `style` 属性補完と標準の感情メニューは、カーソルまたは選択範囲を囲む最内の `<voice name="...">` が対応するスタイルだけを表示します。音声名が未指定の場合は全候補を表示し、登録済みでスタイル非対応の音声や未登録の音声では候補がないことを表示します。`emotionStyles` を指定した場合、感情メニューではその値と登録済み音声の対応スタイルの共通部分を使用します。
249
269
 
250
270
  ```tsx
@@ -347,6 +367,19 @@ const audio = await client.synthesize(ssml);
347
367
 
348
368
  帯域幅を抑える場合は `audio-24khz-48kbitrate-mono-mp3` または `audio-16khz-32kbitrate-mono-mp3` を指定できます。
349
369
 
370
+ 最新の音声一覧は公開 API から取得できます。複数リージョンを指定した場合は音声名で重複排除され、各音声の `regions` と `metadata`(`voiceCount`、`generatedAt`、`apiVersion`、`regions`)が返ります。
371
+
372
+ ```ts
373
+ import { fetchAzureVoiceCatalog } from "ssml-builder-js";
374
+
375
+ const catalog = await fetchAzureVoiceCatalog({
376
+ apiKey: process.env.AZURE_SPEECH_KEY!,
377
+ region: ["eastus", "japaneast"],
378
+ });
379
+ ```
380
+
381
+ CLI では `npx ssml-builder sync-voices --region eastus --output ./azure-voices.json` を実行します。キーは `AZURE_SPEECH_KEY`(または `--key`)、リージョンは `AZURE_SPEECH_REGION(S)`(または `--region(s)`)から指定できます。
382
+
350
383
  ```ts
351
384
  const client = new AzureTtsClient({
352
385
  subscriptionKey: process.env.AZURE_SPEECH_KEY!,
@@ -378,8 +411,8 @@ Next.js Route Handler ではキーをブラウザへ渡さず、サーバー側
378
411
 
379
412
  ```ts
380
413
  // app/api/synthesize/route.ts
381
- import { AzureTtsClient, AzureTtsError } from "@ssml-builder-js/azure-tts-client";
382
- import { validateAzureSsml, validateSsml } from "@ssml-builder-js/ssml-core";
414
+ import { AzureTtsClient, AzureTtsError } from "ssml-builder-js";
415
+ import { validateAzureSsml, validateSsml } from "ssml-builder-js/core";
383
416
 
384
417
  export const runtime = "nodejs";
385
418
 
@@ -539,7 +572,7 @@ For both the Pages Router and App Router, keep Monaco in a client-only module wi
539
572
  "use client";
540
573
  import dynamic from "next/dynamic";
541
574
  const SsmlEditor = dynamic(
542
- () => import("@ssml-builder-js/ssml-editor-react").then(({ SsmlEditor }) => SsmlEditor),
575
+ () => import("ssml-builder-js/react").then(({ SsmlEditor }) => SsmlEditor),
543
576
  { ssr: false },
544
577
  );
545
578
 
@@ -627,6 +660,8 @@ SSML validation separates XML syntax, Azure-specific static semantics, and runti
627
660
 
628
661
  Typed representations are available for elements such as `voice`, `prosody`, `break`, `express-as`, `say-as`, `phoneme`, `audio`, `lang`, and `mark`, plus `mstts:dialog`, `mstts:turn`, `mstts:backgroundaudio`, `mstts:ttsembedding`, `mstts:embedding`, and `mstts:voiceconversion`. Use `type: "custom"` and `name` to handle undefined XML elements or additional attributes. When a document contains `mstts:` elements, the required Azure Speech namespace is added automatically.
629
662
 
663
+ `mstts:turn` accepts either `voice` or the multi-talker `speaker` property. The typed extension properties include `speakerProfileId` for `mstts:ttsembedding`, `id` for `mstts:embedding`, and `url` plus `profile` for `mstts:voiceconversion`. `mstts:backgroundaudio` must be the first direct child element of `<speak>` and may appear only once per document; `fadein` and `fadeout` are raw milliseconds from 0 through 10000.
664
+
630
665
  `validateAzureSsml` is a preflight static check performed by this package before sending SSML to Azure Speech. The `source` on each returned diagnostic identifies package-side static analysis; it is independent of Azure Speech's runtime generation result and does not guarantee that synthesis will succeed.
631
666
 
632
667
  Use `mapSsmlTextNodes` to replace translatable content without changing tags, attributes, or nesting. The transform receives the immediate parent tag and ancestor `path`, and may return a `string` or a `Promise<string>`. The third argument supports `skipTags` and a `filter` callback; `phoneme`, `say-as`, and `sub` are skipped by default. The callback receives `parentTag`, decoded `parentAttributes`, `ancestorTags`, and `path`.
@@ -650,7 +685,7 @@ const diagnostics = validateAzureSsml(ssml, {
650
685
  });
651
686
  ```
652
687
 
653
- The catalog is represented by `AzureVoiceDefinition` with `name`, `locale`, optional `secondaryLocales`, `styles`, `supportedTags`, `unsupportedTags`, and `models`. Pass `voiceDefinitions` (or `voiceCatalog`) to supplement or override the built-in catalog with an external definition. A tag that violates `supportedTags` or `unsupportedTags` produces `azure-unsupported-tag-for-voice` with error severity. `customVoiceStyleMap` remains supported for backward compatibility and overrides styles for the named voice. Diagnostics distinguish an unregistered voice (`azure-unknown-voice`, controlled by `unknownVoicePolicy`), an unsupported style on a registered voice (`azure-unsupported-style`), and a locale mismatch (`azure-locale-mismatch`). The `<mstts:audioduration value="10s"/>` element accepts positive `ms` or `s` values and `hh:mm:ss[.fff]` clock values.
688
+ The catalog is represented by `AzureVoiceDefinition` with `name`, `locale`, optional `secondaryLocales`, `styles`, `supportedTags`, `unsupportedTags`, `models`, `regions`, and `status`. Pass `voiceDefinitions` (or `voiceCatalog`) to supplement or override the built-in catalog with an external definition. A tag that violates `supportedTags` or `unsupportedTags` produces `azure-unsupported-tag-for-voice` with error severity. `customVoiceStyleMap` remains supported for backward compatibility and overrides styles for the named voice. Diagnostics distinguish an unregistered voice (`azure-unknown-voice`, controlled by `unknownVoicePolicy`), an unsupported style on a registered voice (`azure-unsupported-style`), and a locale mismatch (`azure-locale-mismatch`). The `<mstts:audioduration value="10s"/>` element accepts positive `ms` or `s` values and `hh:mm:ss[.fff]` clock values.
654
689
 
655
690
  `npm run sync:voices -- --regions eastus,japaneast` fetches Azure's List Voices API for each region, deduplicates the results, and updates the generated TypeScript definitions plus `azureVoiceCatalog.json` with generation time, API version, regions, and voice count. Provide credentials through `AZURE_SPEECH_KEY` and `AZURE_SPEECH_REGION(S)`, or CLI options.
656
691
 
@@ -689,6 +724,24 @@ Azure Speech fetches `<audio>` URLs, so a server that accepts arbitrary user-pro
689
724
 
690
725
  Set `editMode="visual"`, or choose **Visual** in the toolbar, to edit without viewing XML source. The structured editor provides a structure tree, parent breadcrumb selection, text editing, and form actions for rate, pitch, emotion, pause, and pronunciation. Azure placement and attribute diagnostics are shown in the visual area. Choose `editMode="code"` to return to Monaco.
691
726
 
727
+ The Visual Editor and Code Editor support the following elements:
728
+
729
+ | Element | Visual Editor | Code Editor |
730
+ | --- | --- | --- |
731
+ | `voice`, `prosody`, `break`, `express-as`, `say-as`, `phoneme` | Form actions and structure tree | Full support |
732
+ | `mstts:dialog` / `mstts:turn` | Add turns and edit voice, speaker, and text | Full support with completion |
733
+ | `mstts:backgroundaudio` | Edit URL, volume, fade-in, and fade-out | Full support with completion |
734
+ | `mstts:ttsembedding`, `mstts:embedding`, `mstts:voiceconversion` | Preserved in the structure tree | Full support |
735
+ | Unknown XML elements | Preserved in the structure tree | Editable as `custom` |
736
+
737
+ Azure lifecycle support is reported by static validation: GA elements produce no lifecycle diagnostic, preview elements and voices produce a Warning, and deprecated elements and voices produce an Info diagnostic. Use `AzureVoiceDefinition.status` and `AzureValidationOptions.tagStatuses` for external catalog metadata.
738
+
739
+ | Status | Diagnostic | Examples |
740
+ | --- | --- | --- |
741
+ | GA | None | Standard SSML, `mstts:dialog`, `mstts:backgroundaudio` |
742
+ | Preview | Warning | `mstts:voiceconversion`, tags listed in `previewTags` / `tagStatuses`, voices with `status: "preview"` |
743
+ | Deprecated | Info | Tags listed in `deprecatedTags` / `tagStatuses`, voices with `status: "deprecated"` |
744
+
692
745
  Completion for the `<mstts:express-as>` `style` attribute and the built-in Emotion menu only show styles supported by the innermost `<voice name="...">` around the cursor or selection. When no voice name is available, all candidates are shown; a registered voice without supported styles or an explicitly unregistered voice displays an empty-state message. When `emotionStyles` is supplied, the Emotion menu uses its intersection with the registered voice's supported styles.
693
746
 
694
747
  ```tsx
@@ -791,6 +844,19 @@ const audio = await client.synthesize(ssml);
791
844
 
792
845
  For lower bandwidth, choose `audio-24khz-48kbitrate-mono-mp3` or `audio-16khz-32kbitrate-mono-mp3`.
793
846
 
847
+ Fetch the current voice catalog from Azure with `fetchAzureVoiceCatalog`. Multiple regions are deduplicated by voice name, and each voice includes its available `regions`; the result also contains `metadata` with `voiceCount`, `generatedAt`, `apiVersion`, and `regions`.
848
+
849
+ ```ts
850
+ import { fetchAzureVoiceCatalog } from "ssml-builder-js";
851
+
852
+ const catalog = await fetchAzureVoiceCatalog({
853
+ apiKey: process.env.AZURE_SPEECH_KEY!,
854
+ region: ["eastus", "japaneast"],
855
+ });
856
+ ```
857
+
858
+ The public updater CLI is `npx ssml-builder sync-voices --region eastus --output ./azure-voices.json`. It reads the key from `AZURE_SPEECH_KEY` (or `--key`) and regions from `AZURE_SPEECH_REGION(S)` (or `--region(s)`).
859
+
794
860
  ```ts
795
861
  const client = new AzureTtsClient({
796
862
  subscriptionKey: process.env.AZURE_SPEECH_KEY!,
@@ -822,8 +888,8 @@ Keep the subscription key on the server by using a Next.js Route Handler (or an
822
888
 
823
889
  ```ts
824
890
  // app/api/synthesize/route.ts
825
- import { AzureTtsClient, AzureTtsError } from "@ssml-builder-js/azure-tts-client";
826
- import { validateAzureSsml, validateSsml } from "@ssml-builder-js/ssml-core";
891
+ import { AzureTtsClient, AzureTtsError } from "ssml-builder-js";
892
+ import { validateAzureSsml, validateSsml } from "ssml-builder-js/core";
827
893
 
828
894
  export const runtime = "nodejs";
829
895
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { writeFile } = require("node:fs/promises");
4
+ const { resolve } = require("node:path");
5
+ const { fetchAzureVoiceCatalog } = require("../dist/index.js");
6
+
7
+ function usage() {
8
+ return [
9
+ "Usage: ssml-builder sync-voices --region <region[,region...]> --output <file>",
10
+ "",
11
+ "Environment variables: AZURE_SPEECH_KEY, AZURE_SPEECH_REGION(S)",
12
+ ].join("\n");
13
+ }
14
+
15
+ function parseArgs(args) {
16
+ const options = {};
17
+ for (let index = 0; index < args.length; index += 1) {
18
+ const argument = args[index];
19
+ if (argument === "--region" || argument === "--regions") {
20
+ options.regions = (args[++index] || "")
21
+ .split(",")
22
+ .map((value) => value.trim())
23
+ .filter(Boolean);
24
+ } else if (argument === "--output") {
25
+ options.output = args[++index];
26
+ } else if (argument === "--key") {
27
+ options.apiKey = args[++index];
28
+ } else if (argument === "--help" || argument === "-h") {
29
+ console.log(usage());
30
+ process.exit(0);
31
+ } else {
32
+ throw new Error(`Unknown option: ${argument}\n\n${usage()}`);
33
+ }
34
+ }
35
+ return options;
36
+ }
37
+
38
+ async function main() {
39
+ const [command, ...args] = process.argv.slice(2);
40
+ if (command !== "sync-voices") throw new Error(usage());
41
+ const options = parseArgs(args);
42
+ const regions =
43
+ options.regions ||
44
+ (process.env.AZURE_SPEECH_REGIONS || process.env.AZURE_SPEECH_REGION || "")
45
+ .split(",")
46
+ .map((value) => value.trim())
47
+ .filter(Boolean);
48
+ const apiKey = options.apiKey || process.env.AZURE_SPEECH_KEY;
49
+ const output = options.output;
50
+ if (!apiKey) throw new Error("Set AZURE_SPEECH_KEY or pass --key.");
51
+ if (regions.length === 0) throw new Error("Set AZURE_SPEECH_REGION(S) or pass --region(s).");
52
+ if (!output) throw new Error(`Pass --output <file>.\n\n${usage()}`);
53
+
54
+ const catalog = await fetchAzureVoiceCatalog({ apiKey, region: regions });
55
+ await writeFile(resolve(output), `${JSON.stringify(catalog, null, 2)}\n`, "utf8");
56
+ console.log(`Wrote ${catalog.metadata.voiceCount} Azure voice definitions to ${resolve(output)}.`);
57
+ }
58
+
59
+ main().catch((error) => {
60
+ console.error(error instanceof Error ? error.message : String(error));
61
+ process.exitCode = 1;
62
+ });
@@ -542,6 +542,7 @@ var ssmlPresets_exports = {};
542
542
  __export(ssmlPresets_exports, {
543
543
  AUDIO_DURATION_DESCRIPTIONS: () => AUDIO_DURATION_DESCRIPTIONS,
544
544
  AUDIO_DURATION_PRESETS: () => AUDIO_DURATION_PRESETS,
545
+ BACKGROUND_AUDIO_DURATION_PRESETS: () => BACKGROUND_AUDIO_DURATION_PRESETS,
545
546
  BREAK_STRENGTH_PRESETS: () => BREAK_STRENGTH_PRESETS,
546
547
  BREAK_TIME_DESCRIPTIONS: () => BREAK_TIME_DESCRIPTIONS,
547
548
  BREAK_TIME_PRESETS: () => BREAK_TIME_PRESETS,
@@ -623,6 +624,9 @@ var AZURE_VOICE_STYLE_MAP = {
623
624
  ],
624
625
  "es-ES-ElviraNeural": [],
625
626
  "fil-PH-AngeloNeural": [],
627
+ "fil-PH-Angelo:DragonHDLatestNeural": [],
628
+ "fil-PH-BlessicaNeural": [],
629
+ "fil-PH-Blessica:DragonHDLatestNeural": [],
626
630
  "fr-FR-DeniseNeural": ["cheerful", "sad"],
627
631
  "fr-FR-HenriNeural": ["cheerful", "sad"],
628
632
  "id-ID-GadisNeural": [],
@@ -900,6 +904,7 @@ var SAY_AS_PRESETS = [
900
904
  var LANGUAGE_PRESETS = ["ja-JP", "en-US", "de-DE", "fr-FR"];
901
905
  var SILENCE_VALUE_PRESETS = ["300ms", "500ms", "1s"];
902
906
  var AUDIO_DURATION_PRESETS = ["5s", "10s", "30s"];
907
+ var BACKGROUND_AUDIO_DURATION_PRESETS = ["0", "500", "1000", "3000", "10000"];
903
908
  var SILENCE_TYPE_PRESETS = [
904
909
  "Leading",
905
910
  "Tailing",
@@ -961,8 +966,8 @@ var SSML_ATTRIBUTE_PRESETS = {
961
966
  },
962
967
  "mstts:backgroundaudio": {
963
968
  volume: PROSODY_VOLUME_PRESETS,
964
- fadein: AUDIO_DURATION_PRESETS,
965
- fadeout: AUDIO_DURATION_PRESETS
969
+ fadein: BACKGROUND_AUDIO_DURATION_PRESETS,
970
+ fadeout: BACKGROUND_AUDIO_DURATION_PRESETS
966
971
  },
967
972
  silence: {
968
973
  type: SILENCE_TYPE_PRESETS,
@@ -1364,6 +1369,7 @@ var SSML_ATTRS = {
1364
1369
  MSTTS_XMLNS: "xmlns:mstts",
1365
1370
  NAME: "name",
1366
1371
  VOICE: "voice",
1372
+ SPEAKER: "speaker",
1367
1373
  EFFECT: "effect",
1368
1374
  RATE: "rate",
1369
1375
  PITCH: "pitch",
@@ -1394,6 +1400,11 @@ var SSML_ATTRS = {
1394
1400
  ALIAS: "alias",
1395
1401
  MARK: "mark",
1396
1402
  URI: "uri",
1403
+ ID: "id",
1404
+ MODEL: "model",
1405
+ PROFILE: "profile",
1406
+ URL: "url",
1407
+ SPEAKER_PROFILE_ID: "speakerProfileId",
1397
1408
  TYPE: "type",
1398
1409
  VALUE: "value",
1399
1410
  FADE_IN: "fadein",
@@ -1489,6 +1500,7 @@ function getAttributes(element) {
1489
1500
  break;
1490
1501
  case SSML_TAGS.MSTTS_TURN:
1491
1502
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1503
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
1492
1504
  break;
1493
1505
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1494
1506
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -1497,9 +1509,18 @@ function getAttributes(element) {
1497
1509
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1498
1510
  break;
1499
1511
  case SSML_TAGS.MSTTS_DIALOG:
1512
+ break;
1500
1513
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1514
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1515
+ break;
1501
1516
  case SSML_TAGS.MSTTS_EMBEDDING:
1517
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
1518
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1519
+ break;
1502
1520
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1521
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
1522
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
1523
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1503
1524
  break;
1504
1525
  case SSML_TAGS.PARAGRAPH:
1505
1526
  case SSML_TAGS.SENTENCE:
@@ -2072,7 +2093,9 @@ function convertElement(node) {
2072
2093
  case SSML_TAGS.MSTTS_TURN: {
2073
2094
  const element = { type: SSML_TAGS.MSTTS_TURN };
2074
2095
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2096
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
2075
2097
  if (voice !== void 0) element.voice = voice;
2098
+ if (speaker !== void 0) element.speaker = speaker;
2076
2099
  return finishElement(element, node, attributes);
2077
2100
  }
2078
2101
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -2089,14 +2112,26 @@ function convertElement(node) {
2089
2112
  }
2090
2113
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2091
2114
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2115
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2116
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2092
2117
  return finishElement(element, node, attributes);
2093
2118
  }
2094
2119
  case SSML_TAGS.MSTTS_EMBEDDING: {
2095
2120
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2121
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
2122
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2123
+ if (id !== void 0) element.id = id;
2124
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2096
2125
  return finishElement(element, node, attributes);
2097
2126
  }
2098
2127
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2099
2128
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2129
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
2130
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
2131
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2132
+ if (url !== void 0) element.url = url;
2133
+ if (profile !== void 0) element.profile = profile;
2134
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2100
2135
  return finishElement(element, node, attributes);
2101
2136
  }
2102
2137
  default: {
@@ -2278,6 +2313,9 @@ var AZURE_VOICE_DEFINITIONS = [
2278
2313
  },
2279
2314
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
2280
2315
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
2316
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
2317
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
2318
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
2281
2319
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2282
2320
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
2283
2321
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -2372,6 +2410,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
2372
2410
  "Enumerationcomma"
2373
2411
  ]);
2374
2412
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
2413
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
2414
+ function featureStatusForTag(name, options) {
2415
+ const tagName = canonicalTagName(name);
2416
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
2417
+ ([candidate]) => canonicalTagName(candidate) === tagName
2418
+ )?.[1];
2419
+ if (configured) return configured;
2420
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
2421
+ return "preview";
2422
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
2423
+ return void 0;
2424
+ }
2375
2425
  function decodeAttribute(value) {
2376
2426
  return value.replace(
2377
2427
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -2431,11 +2481,14 @@ function tokenizeElements(source) {
2431
2481
  }
2432
2482
  const selfClosing = /\/\s*>$/.test(raw);
2433
2483
  const parent = openElements[openElements.length - 1];
2484
+ const childElementIndex = parent?.childElementCount;
2485
+ if (parent) parent.childElementCount += 1;
2434
2486
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2435
2487
  const tokenName = nameMatch[1];
2436
2488
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2437
2489
  tokens.push({
2438
2490
  attributes,
2491
+ childElementIndex,
2439
2492
  end,
2440
2493
  name: tokenName,
2441
2494
  parentName: parent?.name,
@@ -2445,6 +2498,7 @@ function tokenizeElements(source) {
2445
2498
  });
2446
2499
  if (!selfClosing) {
2447
2500
  openElements.push({
2501
+ childElementCount: 0,
2448
2502
  name: tokenName,
2449
2503
  voiceName: tokenVoiceName
2450
2504
  });
@@ -2483,6 +2537,12 @@ function isValidAzureAudioDuration(value) {
2483
2537
  if (!clock) return false;
2484
2538
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2485
2539
  }
2540
+ function isValidAzureBackgroundAudioDuration(value) {
2541
+ const match = /^(\d+)$/.exec(value.trim());
2542
+ if (!match) return false;
2543
+ const milliseconds = Number(match[1]);
2544
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
2545
+ }
2486
2546
  function attr(token, name) {
2487
2547
  return token.attributes.get(name.toLowerCase());
2488
2548
  }
@@ -2612,9 +2672,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2612
2672
  }
2613
2673
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2614
2674
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2615
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
2675
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
2676
+ try {
2677
+ return new URL(allowedOrigin).origin === parsed.origin;
2678
+ } catch {
2679
+ return allowedOrigin === parsed.origin;
2680
+ }
2681
+ }) ?? false;
2682
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
2616
2683
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2617
- else if (!options.allowExternalAudio)
2684
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
2618
2685
  addDiagnostic(
2619
2686
  diagnostics,
2620
2687
  source,
@@ -2624,6 +2691,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2624
2691
  }
2625
2692
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
2626
2693
  const name = token.name.toLowerCase();
2694
+ const tagStatus = featureStatusForTag(token.name, options);
2695
+ if (tagStatus === "preview")
2696
+ addDiagnostic(
2697
+ diagnostics,
2698
+ source,
2699
+ token.start,
2700
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
2701
+ "warning",
2702
+ "azure-preview-tag"
2703
+ );
2704
+ if (tagStatus === "deprecated")
2705
+ addDiagnostic(
2706
+ diagnostics,
2707
+ source,
2708
+ token.start,
2709
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
2710
+ "info",
2711
+ "azure-deprecated-tag"
2712
+ );
2627
2713
  if (name === "voice" && !attr(token, "name")?.trim())
2628
2714
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
2629
2715
  if (name === "break") {
@@ -2744,28 +2830,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
2744
2830
  validateAudioSource(token, source, diagnostics, options, "audio");
2745
2831
  }
2746
2832
  if (name === "mstts:turn") {
2747
- if (!attr(token, "voice")?.trim())
2748
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
2833
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
2834
+ addDiagnostic(
2835
+ diagnostics,
2836
+ source,
2837
+ token.start,
2838
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
2839
+ );
2749
2840
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
2750
2841
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
2751
2842
  }
2752
2843
  if (name === "mstts:backgroundaudio") {
2753
2844
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
2754
2845
  const volume = attr(token, "volume");
2755
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
2846
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
2756
2847
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
2757
2848
  for (const [attribute, value] of [
2758
2849
  ["fadein", attr(token, "fadein")],
2759
2850
  ["fadeout", attr(token, "fadeout")]
2760
2851
  ]) {
2761
- if (value && !isValidAzureAudioDuration(value))
2852
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
2762
2853
  addDiagnostic(
2763
2854
  diagnostics,
2764
2855
  source,
2765
2856
  token.start,
2766
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2857
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
2767
2858
  );
2768
2859
  }
2860
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2861
+ addDiagnostic(
2862
+ diagnostics,
2863
+ source,
2864
+ token.start,
2865
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2866
+ );
2769
2867
  if (!token.selfClosing)
2770
2868
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
2771
2869
  }
@@ -2797,6 +2895,16 @@ function validateAzureSsml(ssml, options = {}) {
2797
2895
  const tokens = tokenizeElements(ssml);
2798
2896
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2799
2897
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2898
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2899
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2900
+ if (index > 0)
2901
+ addDiagnostic(
2902
+ diagnostics,
2903
+ ssml,
2904
+ token.start,
2905
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2906
+ );
2907
+ }
2800
2908
  if (!speak || voices.length === 0)
2801
2909
  addDiagnostic(
2802
2910
  diagnostics,
@@ -2813,6 +2921,24 @@ function validateAzureSsml(ssml, options = {}) {
2813
2921
  const name = attr(token, "name")?.trim();
2814
2922
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
2815
2923
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2924
+ if (name && definition?.status === "preview")
2925
+ addDiagnostic(
2926
+ diagnostics,
2927
+ ssml,
2928
+ token.start,
2929
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
2930
+ "warning",
2931
+ "azure-preview-voice"
2932
+ );
2933
+ if (name && definition?.status === "deprecated")
2934
+ addDiagnostic(
2935
+ diagnostics,
2936
+ ssml,
2937
+ token.start,
2938
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
2939
+ "info",
2940
+ "azure-deprecated-voice"
2941
+ );
2816
2942
  if (name && !definition && policySeverity)
2817
2943
  addDiagnostic(
2818
2944
  diagnostics,
@@ -3654,7 +3780,7 @@ var SSML_COMPLETION_SNIPPETS = [
3654
3780
  },
3655
3781
  {
3656
3782
  label: "mstts:backgroundaudio",
3657
- insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
3783
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:70}" fadein="\${3:1000}" fadeout="\${4:1000}" />`
3658
3784
  },
3659
3785
  {
3660
3786
  label: "mstts:ttsembedding",
@@ -4058,18 +4184,18 @@ var SSML_TAG_DEFINITIONS = [
4058
4184
  },
4059
4185
  {
4060
4186
  name: "volume",
4061
- description: "The background audio volume, for example `-3dB` or `medium`.",
4062
- example: "-3dB"
4187
+ description: "The background audio volume from 0 to 100.",
4188
+ example: "70"
4063
4189
  },
4064
4190
  {
4065
4191
  name: "fadein",
4066
- description: "The fade-in duration, for example `1s`.",
4067
- example: "1s"
4192
+ description: "The fade-in duration in milliseconds from 0 to 10000.",
4193
+ example: "1000"
4068
4194
  },
4069
4195
  {
4070
4196
  name: "fadeout",
4071
- description: "The fade-out duration, for example `500ms`.",
4072
- example: "500ms"
4197
+ description: "The fade-out duration in milliseconds from 0 to 10000.",
4198
+ example: "1000"
4073
4199
  }
4074
4200
  ]
4075
4201
  },
@@ -4439,4 +4565,4 @@ export {
4439
4565
  findSsmlHoverTarget,
4440
4566
  formatSsmlHover
4441
4567
  };
4442
- //# sourceMappingURL=chunk-JNJVTEL6.mjs.map
4568
+ //# sourceMappingURL=chunk-FWWMWI2C.mjs.map