ssml-builder-js 2.9.0 → 2.10.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 つだけ配置します。
183
+
182
184
  `validateAzureSsml` は Azure Speech へ送信する前に実行する事前静的チェックです。返却される診断の `source` はパッケージ側の静的解析結果であることを示し、Azure Speech サービス側でのランタイム生成結果や実際の合成可否を表すものではありません。
183
185
 
184
186
  翻訳などで本文だけを置き換える場合は、`mapSsmlTextNodes` に変換関数を渡します。変換関数には直近の親タグと祖先タグの `path` が渡され、戻り値は `string` または `Promise<string>` を指定できます。`validateAzureSsml` は音声、属性値、音声スタイル、文字数、`audio` URL/オリジンを検証します。
@@ -378,8 +380,8 @@ Next.js Route Handler ではキーをブラウザへ渡さず、サーバー側
378
380
 
379
381
  ```ts
380
382
  // 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";
383
+ import { AzureTtsClient, AzureTtsError } from "ssml-builder-js";
384
+ import { validateAzureSsml, validateSsml } from "ssml-builder-js/core";
383
385
 
384
386
  export const runtime = "nodejs";
385
387
 
@@ -539,7 +541,7 @@ For both the Pages Router and App Router, keep Monaco in a client-only module wi
539
541
  "use client";
540
542
  import dynamic from "next/dynamic";
541
543
  const SsmlEditor = dynamic(
542
- () => import("@ssml-builder-js/ssml-editor-react").then(({ SsmlEditor }) => SsmlEditor),
544
+ () => import("ssml-builder-js/react").then(({ SsmlEditor }) => SsmlEditor),
543
545
  { ssr: false },
544
546
  );
545
547
 
@@ -627,6 +629,8 @@ SSML validation separates XML syntax, Azure-specific static semantics, and runti
627
629
 
628
630
  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
631
 
632
+ `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.
633
+
630
634
  `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
635
 
632
636
  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`.
@@ -822,8 +826,8 @@ Keep the subscription key on the server by using a Next.js Route Handler (or an
822
826
 
823
827
  ```ts
824
828
  // 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";
829
+ import { AzureTtsClient, AzureTtsError } from "ssml-builder-js";
830
+ import { validateAzureSsml, validateSsml } from "ssml-builder-js/core";
827
831
 
828
832
  export const runtime = "nodejs";
829
833
 
@@ -1364,6 +1364,7 @@ var SSML_ATTRS = {
1364
1364
  MSTTS_XMLNS: "xmlns:mstts",
1365
1365
  NAME: "name",
1366
1366
  VOICE: "voice",
1367
+ SPEAKER: "speaker",
1367
1368
  EFFECT: "effect",
1368
1369
  RATE: "rate",
1369
1370
  PITCH: "pitch",
@@ -1394,6 +1395,11 @@ var SSML_ATTRS = {
1394
1395
  ALIAS: "alias",
1395
1396
  MARK: "mark",
1396
1397
  URI: "uri",
1398
+ ID: "id",
1399
+ MODEL: "model",
1400
+ PROFILE: "profile",
1401
+ URL: "url",
1402
+ SPEAKER_PROFILE_ID: "speakerProfileId",
1397
1403
  TYPE: "type",
1398
1404
  VALUE: "value",
1399
1405
  FADE_IN: "fadein",
@@ -1489,6 +1495,7 @@ function getAttributes(element) {
1489
1495
  break;
1490
1496
  case SSML_TAGS.MSTTS_TURN:
1491
1497
  addAttribute(attributes, SSML_ATTRS.VOICE, element.voice);
1498
+ addAttribute(attributes, SSML_ATTRS.SPEAKER, element.speaker);
1492
1499
  break;
1493
1500
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO:
1494
1501
  addAttribute(attributes, SSML_ATTRS.SRC, element.src);
@@ -1497,9 +1504,18 @@ function getAttributes(element) {
1497
1504
  addAttribute(attributes, SSML_ATTRS.FADE_OUT, element.fadeOut ?? element.fadeout);
1498
1505
  break;
1499
1506
  case SSML_TAGS.MSTTS_DIALOG:
1507
+ break;
1500
1508
  case SSML_TAGS.MSTTS_TTS_EMBEDDING:
1509
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1510
+ break;
1501
1511
  case SSML_TAGS.MSTTS_EMBEDDING:
1512
+ addAttribute(attributes, SSML_ATTRS.ID, element.id);
1513
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1514
+ break;
1502
1515
  case SSML_TAGS.MSTTS_VOICE_CONVERSION:
1516
+ addAttribute(attributes, SSML_ATTRS.URL, element.url);
1517
+ addAttribute(attributes, SSML_ATTRS.PROFILE, element.profile);
1518
+ addAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID, element.speakerProfileId);
1503
1519
  break;
1504
1520
  case SSML_TAGS.PARAGRAPH:
1505
1521
  case SSML_TAGS.SENTENCE:
@@ -2072,7 +2088,9 @@ function convertElement(node) {
2072
2088
  case SSML_TAGS.MSTTS_TURN: {
2073
2089
  const element = { type: SSML_TAGS.MSTTS_TURN };
2074
2090
  const voice = readAttribute(attributes, SSML_ATTRS.VOICE);
2091
+ const speaker = readAttribute(attributes, SSML_ATTRS.SPEAKER);
2075
2092
  if (voice !== void 0) element.voice = voice;
2093
+ if (speaker !== void 0) element.speaker = speaker;
2076
2094
  return finishElement(element, node, attributes);
2077
2095
  }
2078
2096
  case SSML_TAGS.MSTTS_BACKGROUND_AUDIO: {
@@ -2089,14 +2107,26 @@ function convertElement(node) {
2089
2107
  }
2090
2108
  case SSML_TAGS.MSTTS_TTS_EMBEDDING: {
2091
2109
  const element = { type: SSML_TAGS.MSTTS_TTS_EMBEDDING };
2110
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2111
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2092
2112
  return finishElement(element, node, attributes);
2093
2113
  }
2094
2114
  case SSML_TAGS.MSTTS_EMBEDDING: {
2095
2115
  const element = { type: SSML_TAGS.MSTTS_EMBEDDING };
2116
+ const id = readAttribute(attributes, SSML_ATTRS.ID);
2117
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2118
+ if (id !== void 0) element.id = id;
2119
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2096
2120
  return finishElement(element, node, attributes);
2097
2121
  }
2098
2122
  case SSML_TAGS.MSTTS_VOICE_CONVERSION: {
2099
2123
  const element = { type: SSML_TAGS.MSTTS_VOICE_CONVERSION };
2124
+ const url = readAttribute(attributes, SSML_ATTRS.URL);
2125
+ const profile = readAttribute(attributes, SSML_ATTRS.PROFILE);
2126
+ const speakerProfileId = readAttribute(attributes, SSML_ATTRS.SPEAKER_PROFILE_ID);
2127
+ if (url !== void 0) element.url = url;
2128
+ if (profile !== void 0) element.profile = profile;
2129
+ if (speakerProfileId !== void 0) element.speakerProfileId = speakerProfileId;
2100
2130
  return finishElement(element, node, attributes);
2101
2131
  }
2102
2132
  default: {
@@ -2431,11 +2461,14 @@ function tokenizeElements(source) {
2431
2461
  }
2432
2462
  const selfClosing = /\/\s*>$/.test(raw);
2433
2463
  const parent = openElements[openElements.length - 1];
2464
+ const childElementIndex = parent?.childElementCount;
2465
+ if (parent) parent.childElementCount += 1;
2434
2466
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2435
2467
  const tokenName = nameMatch[1];
2436
2468
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2437
2469
  tokens.push({
2438
2470
  attributes,
2471
+ childElementIndex,
2439
2472
  end,
2440
2473
  name: tokenName,
2441
2474
  parentName: parent?.name,
@@ -2445,6 +2478,7 @@ function tokenizeElements(source) {
2445
2478
  });
2446
2479
  if (!selfClosing) {
2447
2480
  openElements.push({
2481
+ childElementCount: 0,
2448
2482
  name: tokenName,
2449
2483
  voiceName: tokenVoiceName
2450
2484
  });
@@ -2483,6 +2517,12 @@ function isValidAzureAudioDuration(value) {
2483
2517
  if (!clock) return false;
2484
2518
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
2485
2519
  }
2520
+ function isValidAzureBackgroundAudioDuration(value) {
2521
+ const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
2522
+ if (!match) return false;
2523
+ const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
2524
+ return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
2525
+ }
2486
2526
  function attr(token, name) {
2487
2527
  return token.attributes.get(name.toLowerCase());
2488
2528
  }
@@ -2612,9 +2652,16 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
2612
2652
  }
2613
2653
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
2614
2654
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
2615
- if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
2655
+ const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
2656
+ try {
2657
+ return new URL(allowedOrigin).origin === parsed.origin;
2658
+ } catch {
2659
+ return allowedOrigin === parsed.origin;
2660
+ }
2661
+ }) ?? false;
2662
+ if (options.allowedAudioOrigins && !isAllowedOrigin)
2616
2663
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
2617
- else if (!options.allowExternalAudio)
2664
+ else if (!isAllowedOrigin && !options.allowExternalAudio)
2618
2665
  addDiagnostic(
2619
2666
  diagnostics,
2620
2667
  source,
@@ -2744,28 +2791,40 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
2744
2791
  validateAudioSource(token, source, diagnostics, options, "audio");
2745
2792
  }
2746
2793
  if (name === "mstts:turn") {
2747
- if (!attr(token, "voice")?.trim())
2748
- addDiagnostic(diagnostics, source, token.start, '<mstts:turn> requires a non-empty "voice" attribute.');
2794
+ if (!attr(token, "voice")?.trim() && !attr(token, "speaker")?.trim())
2795
+ addDiagnostic(
2796
+ diagnostics,
2797
+ source,
2798
+ token.start,
2799
+ '<mstts:turn> requires a non-empty "voice" or "speaker" attribute.'
2800
+ );
2749
2801
  if (token.parentName?.toLowerCase() !== "mstts:dialog")
2750
2802
  addDiagnostic(diagnostics, source, token.start, "<mstts:turn> is only allowed directly inside <mstts:dialog>.");
2751
2803
  }
2752
2804
  if (name === "mstts:backgroundaudio") {
2753
2805
  validateAudioSource(token, source, diagnostics, options, "mstts:backgroundaudio");
2754
2806
  const volume = attr(token, "volume");
2755
- if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%))$/i.test(volume.trim()))
2807
+ if (volume !== void 0 && (!/^\d+(?:\.\d+)?$/.test(volume.trim()) || Number(volume) > 100))
2756
2808
  addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:backgroundaudio volume> value "${volume}".`);
2757
2809
  for (const [attribute, value] of [
2758
2810
  ["fadein", attr(token, "fadein")],
2759
2811
  ["fadeout", attr(token, "fadeout")]
2760
2812
  ]) {
2761
- if (value && !isValidAzureAudioDuration(value))
2813
+ if (value !== void 0 && !isValidAzureBackgroundAudioDuration(value))
2762
2814
  addDiagnostic(
2763
2815
  diagnostics,
2764
2816
  source,
2765
2817
  token.start,
2766
- `<mstts:backgroundaudio ${attribute}> must be a positive duration such as "500ms" or "10s".`
2818
+ `<mstts:backgroundaudio ${attribute}> must be between 0 and 10000 milliseconds, for example "500ms" or "10s".`
2767
2819
  );
2768
2820
  }
2821
+ if (token.parentName?.toLowerCase() !== "speak" || token.childElementIndex !== 0)
2822
+ addDiagnostic(
2823
+ diagnostics,
2824
+ source,
2825
+ token.start,
2826
+ "<mstts:backgroundaudio> must be the first element directly under <speak>."
2827
+ );
2769
2828
  if (!token.selfClosing)
2770
2829
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
2771
2830
  }
@@ -2797,6 +2856,16 @@ function validateAzureSsml(ssml, options = {}) {
2797
2856
  const tokens = tokenizeElements(ssml);
2798
2857
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2799
2858
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2859
+ const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
2860
+ for (const [index, token] of backgroundAudioTokens.entries()) {
2861
+ if (index > 0)
2862
+ addDiagnostic(
2863
+ diagnostics,
2864
+ ssml,
2865
+ token.start,
2866
+ "An SSML document can contain at most one <mstts:backgroundaudio> element."
2867
+ );
2868
+ }
2800
2869
  if (!speak || voices.length === 0)
2801
2870
  addDiagnostic(
2802
2871
  diagnostics,
@@ -4439,4 +4508,4 @@ export {
4439
4508
  findSsmlHoverTarget,
4440
4509
  formatSsmlHover
4441
4510
  };
4442
- //# sourceMappingURL=chunk-JNJVTEL6.mjs.map
4511
+ //# sourceMappingURL=chunk-BYIZQL2W.mjs.map