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 +10 -6
- package/dist/{chunk-JNJVTEL6.mjs → chunk-BYIZQL2W.mjs} +77 -8
- package/dist/chunk-BYIZQL2W.mjs.map +1 -0
- package/dist/{chunk-7LCSH4SZ.mjs → chunk-GW6CKHXF.mjs} +90 -8
- package/dist/chunk-GW6CKHXF.mjs.map +1 -0
- package/dist/core.d.mts +7 -0
- package/dist/core.d.ts +7 -0
- package/dist/core.js +89 -7
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/elements.js +76 -7
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +1 -1
- package/dist/index.js +89 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +7 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +76 -7
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-7LCSH4SZ.mjs.map +0 -1
- package/dist/chunk-JNJVTEL6.mjs.map +0 -1
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("
|
|
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 "
|
|
382
|
-
import { validateAzureSsml, validateSsml } from "
|
|
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("
|
|
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 "
|
|
826
|
-
import { validateAzureSsml, validateSsml } from "
|
|
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
|
-
|
|
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(
|
|
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 &&
|
|
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 && !
|
|
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
|
|
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-
|
|
4511
|
+
//# sourceMappingURL=chunk-BYIZQL2W.mjs.map
|