ssml-builder-js 2.13.0 → 2.14.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 +8 -0
- package/dist/{chunk-25LOR4AJ.mjs → chunk-AQ55MOPU.mjs} +115 -14
- package/dist/chunk-AQ55MOPU.mjs.map +1 -0
- package/dist/{chunk-CZ2F3TET.mjs → chunk-QFIBPCO4.mjs} +80 -9
- package/dist/chunk-QFIBPCO4.mjs.map +1 -0
- package/dist/{chunk-LCTE26LS.mjs → chunk-UZSCXPC5.mjs} +2 -2
- package/dist/core.d.mts +18 -1
- package/dist/core.d.ts +18 -1
- package/dist/core.js +115 -13
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +79 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-Xbr6ZWnK.d.mts → index.d-CUXRwSw0.d.mts} +19 -1
- package/dist/{index.d-Xbr6ZWnK.d.ts → index.d-CUXRwSw0.d.ts} +19 -1
- package/dist/index.d.mts +66 -10
- package/dist/index.d.ts +66 -10
- package/dist/index.js +557 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -17
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +5 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.js +106 -12
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +29 -6
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-25LOR4AJ.mjs.map +0 -1
- package/dist/chunk-CZ2F3TET.mjs.map +0 -1
- /package/dist/{chunk-LCTE26LS.mjs.map → chunk-UZSCXPC5.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -911,8 +911,16 @@ result.bookmarks; // { name, audioOffsetMs }[]
|
|
|
911
911
|
|
|
912
912
|
長文を分割して合成する場合は `synthesizeSsmlChunks` または `AzureTtsClient.synthesizeChunks` を使います。音声バイナリを連結し、`boundaries`、`visemes`、`bookmarks` のオフセットを累積 `durationMs` 分だけ補正します。`synthesizeSsmlSafe(client, ssml, { validation })` は検証エラー時に Azure API を呼び出さず、`status: "validation-error"` / `"azure-api-error"` / `"success"` の結果を返します。
|
|
913
913
|
|
|
914
|
+
v2.14.0 では `synthesizeSsmlChunksSafe(client, chunks, options)` が全チャンクを事前検証し、エラー時に `ChunkValidationError.chunkIndex` を返します。`onProgress` には `chunkIndex`、`originalTextRange`、`status`、`durationMs`、`error` が含まれます。`mergeAudioBuffers(buffers, format)` は PCM WAV のヘッダーを再構築し、MP3 の ID3 タグを除去して結合します。Ogg/WebM など安全に連結できない形式は `UnsupportedMergeFormatError` になります。同期イベントには `chunkIndex`、`sourceNodePath`、`originalTextRange`、`chunkAudioOffsetMs` が付与されます。
|
|
915
|
+
|
|
916
|
+
`validateAzureSsml` の `urlValidation` オプションは URL の重複排除、キャッシュ、`concurrency`、`signal`、`timeoutMs` を制御します。
|
|
917
|
+
|
|
914
918
|
For long documents, use `synthesizeSsmlChunks` or `AzureTtsClient.synthesizeChunks`; `onProgress` reports completed chunks while audio and synchronization offsets are merged. `synthesizeSsmlSafe(client, ssml, { validation })` validates before synthesis and returns a discriminated result without calling Azure when static validation fails.
|
|
915
919
|
|
|
920
|
+
In v2.14.0, `synthesizeSsmlChunksSafe(client, chunks, options)` validates every chunk before contacting Azure and returns a `ChunkValidationError` with its `chunkIndex` when validation fails. `onProgress` events include `chunkIndex`, `originalTextRange`, `status`, `durationMs`, and `error`. `mergeAudioBuffers(buffers, format)` rebuilds PCM WAV headers and strips ID3 tags from MP3 streams; formats such as Ogg and WebM throw `UnsupportedMergeFormatError` because they require re-multiplexing. Synchronization events retain `chunkIndex`, `sourceNodePath`, `originalTextRange`, and `chunkAudioOffsetMs`.
|
|
921
|
+
|
|
922
|
+
The `urlValidation` option of `validateAzureSsml` provides URL deduplication, in-memory caching, bounded `concurrency`, `signal`, and `timeoutMs` controls.
|
|
923
|
+
|
|
916
924
|
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)`).
|
|
917
925
|
|
|
918
926
|
```ts
|
|
@@ -1034,6 +1034,34 @@ function collectInheritedContext(nodes) {
|
|
|
1034
1034
|
nodes.forEach(visit);
|
|
1035
1035
|
return context;
|
|
1036
1036
|
}
|
|
1037
|
+
function elementName(node) {
|
|
1038
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1039
|
+
}
|
|
1040
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1041
|
+
let textOffset = 0;
|
|
1042
|
+
let firstPath;
|
|
1043
|
+
let foundPath;
|
|
1044
|
+
const visit = (node, path) => {
|
|
1045
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1046
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1047
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1048
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1049
|
+
textOffset += text.length;
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
node.children?.forEach((child, index) => {
|
|
1053
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1054
|
+
visit(child, childPath);
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
nodes.forEach((node, index) => {
|
|
1058
|
+
if (!foundPath) {
|
|
1059
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1060
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
return foundPath ?? firstPath;
|
|
1064
|
+
}
|
|
1037
1065
|
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1038
1066
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1039
1067
|
const text = nodes.map(textFromNode).join("");
|
|
@@ -1049,7 +1077,8 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1049
1077
|
containedMarks: marks,
|
|
1050
1078
|
hasBackgroundAudio: chunkNodes.some(
|
|
1051
1079
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1052
|
-
)
|
|
1080
|
+
),
|
|
1081
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
1053
1082
|
};
|
|
1054
1083
|
}
|
|
1055
1084
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1275,7 +1304,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1275
1304
|
|
|
1276
1305
|
// packages/ssml-core/src/migration.ts
|
|
1277
1306
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1278
|
-
function
|
|
1307
|
+
function elementName2(element) {
|
|
1279
1308
|
switch (element.type) {
|
|
1280
1309
|
case "custom":
|
|
1281
1310
|
case "element":
|
|
@@ -1428,7 +1457,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1428
1457
|
}
|
|
1429
1458
|
return;
|
|
1430
1459
|
}
|
|
1431
|
-
const tag =
|
|
1460
|
+
const tag = elementName2(node);
|
|
1432
1461
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1433
1462
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1434
1463
|
});
|
|
@@ -1474,7 +1503,7 @@ function serializeDocument2(document) {
|
|
|
1474
1503
|
const serialize = (node) => {
|
|
1475
1504
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1476
1505
|
if (node.type === "text") return serialize(node.value);
|
|
1477
|
-
const tag =
|
|
1506
|
+
const tag = elementName2(node);
|
|
1478
1507
|
const nodeAttributes = elementAttributes(node);
|
|
1479
1508
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1480
1509
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1488,7 +1517,7 @@ function flatten(document) {
|
|
|
1488
1517
|
nodes.forEach((node, index) => {
|
|
1489
1518
|
if (typeof node === "string" || node.type === "text") return;
|
|
1490
1519
|
const currentPath = `${path}/${index}`;
|
|
1491
|
-
result.push({ name:
|
|
1520
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1492
1521
|
visit(childrenOf(node), currentPath);
|
|
1493
1522
|
});
|
|
1494
1523
|
};
|
|
@@ -1686,6 +1715,69 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1686
1715
|
];
|
|
1687
1716
|
|
|
1688
1717
|
// packages/ssml-core/src/azureValidation.ts
|
|
1718
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1719
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1720
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1721
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1722
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1723
|
+
const waiters = [];
|
|
1724
|
+
let active = 0;
|
|
1725
|
+
const acquire = async () => {
|
|
1726
|
+
if (active < concurrency) {
|
|
1727
|
+
active += 1;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
1731
|
+
active += 1;
|
|
1732
|
+
};
|
|
1733
|
+
const release = () => {
|
|
1734
|
+
active -= 1;
|
|
1735
|
+
waiters.shift()?.();
|
|
1736
|
+
};
|
|
1737
|
+
const check = async (url, context) => {
|
|
1738
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1739
|
+
const cached = cache.get(url);
|
|
1740
|
+
if (cached !== void 0) return cached;
|
|
1741
|
+
const existing = inFlight.get(url);
|
|
1742
|
+
if (existing) return existing;
|
|
1743
|
+
const promise = (async () => {
|
|
1744
|
+
await acquire();
|
|
1745
|
+
try {
|
|
1746
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1747
|
+
const validation = Promise.resolve(validator(url, context));
|
|
1748
|
+
let timer;
|
|
1749
|
+
let abortHandler;
|
|
1750
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1751
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1752
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1753
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1754
|
+
timer = setTimeout(
|
|
1755
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1756
|
+
options.timeoutMs
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
try {
|
|
1761
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1762
|
+
cache.set(url, result);
|
|
1763
|
+
return result;
|
|
1764
|
+
} finally {
|
|
1765
|
+
if (timer) clearTimeout(timer);
|
|
1766
|
+
if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
|
|
1767
|
+
}
|
|
1768
|
+
} finally {
|
|
1769
|
+
release();
|
|
1770
|
+
}
|
|
1771
|
+
})();
|
|
1772
|
+
inFlight.set(url, promise);
|
|
1773
|
+
try {
|
|
1774
|
+
return await promise;
|
|
1775
|
+
} finally {
|
|
1776
|
+
inFlight.delete(url);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
return (url, context) => check(url, context);
|
|
1780
|
+
}
|
|
1689
1781
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1690
1782
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1691
1783
|
"characters",
|
|
@@ -1980,23 +2072,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
1980
2072
|
);
|
|
1981
2073
|
}
|
|
1982
2074
|
}
|
|
1983
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2075
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
1984
2076
|
const src = attr(token, "src");
|
|
1985
2077
|
if (!src) {
|
|
1986
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2078
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
1987
2079
|
return;
|
|
1988
2080
|
}
|
|
1989
2081
|
let parsed;
|
|
1990
2082
|
try {
|
|
1991
2083
|
parsed = new URL(src);
|
|
1992
2084
|
} catch {
|
|
1993
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2085
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
1994
2086
|
return;
|
|
1995
2087
|
}
|
|
1996
2088
|
if (parsed.username || parsed.password)
|
|
1997
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2089
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
1998
2090
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1999
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2091
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2000
2092
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2001
2093
|
try {
|
|
2002
2094
|
const configured = new URL(allowedOrigin);
|
|
@@ -2008,13 +2100,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2008
2100
|
}
|
|
2009
2101
|
}) ?? false;
|
|
2010
2102
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2011
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2103
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2012
2104
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2013
2105
|
addDiagnostic(
|
|
2014
2106
|
diagnostics,
|
|
2015
2107
|
source,
|
|
2016
2108
|
token.start,
|
|
2017
|
-
`<${
|
|
2109
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2018
2110
|
);
|
|
2019
2111
|
}
|
|
2020
2112
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2332,6 +2424,14 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2332
2424
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2333
2425
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2334
2426
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2427
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2428
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2429
|
+
...runnerOptions,
|
|
2430
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2431
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2432
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2433
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2434
|
+
});
|
|
2335
2435
|
let tokens;
|
|
2336
2436
|
try {
|
|
2337
2437
|
tokens = tokenizeElements(ssml);
|
|
@@ -2341,7 +2441,7 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2341
2441
|
const checks = tokens.flatMap(
|
|
2342
2442
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2343
2443
|
try {
|
|
2344
|
-
const result = await
|
|
2444
|
+
const result = await boundedValidator(value, { tag: token.name, attribute });
|
|
2345
2445
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2346
2446
|
if (!valid) {
|
|
2347
2447
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2394,6 +2494,7 @@ export {
|
|
|
2394
2494
|
extractSsmlTranslatableText,
|
|
2395
2495
|
fromPlainTextToSsml,
|
|
2396
2496
|
validateSsmlStructureIntegrity,
|
|
2497
|
+
createAzureUrlValidatorRunner,
|
|
2397
2498
|
isValidAzureAudioDuration,
|
|
2398
2499
|
normalizeAzureLanguage,
|
|
2399
2500
|
areAzureLanguagesEquivalent,
|
|
@@ -2401,4 +2502,4 @@ export {
|
|
|
2401
2502
|
getAzureVoiceCatalogMetadata,
|
|
2402
2503
|
getBuiltInVoiceCatalogMetadata
|
|
2403
2504
|
};
|
|
2404
|
-
//# sourceMappingURL=chunk-
|
|
2505
|
+
//# sourceMappingURL=chunk-AQ55MOPU.mjs.map
|