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/dist/index.js
CHANGED
|
@@ -40,9 +40,13 @@ __export(src_exports, {
|
|
|
40
40
|
AzureTtsClient: () => AzureTtsClient,
|
|
41
41
|
AzureTtsError: () => AzureTtsError,
|
|
42
42
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
43
|
+
ChunkValidationError: () => ChunkValidationError,
|
|
44
|
+
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
43
45
|
areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
|
|
44
46
|
buildPartialSsml: () => buildPartialSsml,
|
|
45
47
|
buildSsml: () => buildSsml,
|
|
48
|
+
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
49
|
+
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
46
50
|
extractSsmlText: () => extractSsmlText,
|
|
47
51
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
48
52
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
@@ -51,13 +55,16 @@ __export(src_exports, {
|
|
|
51
55
|
getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
|
|
52
56
|
isValidAzureAudioDuration: () => isValidAzureAudioDuration,
|
|
53
57
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
58
|
+
mergeAudioBuffers: () => mergeAudioBuffers,
|
|
54
59
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
55
60
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
56
61
|
parseSsml: () => parseSsml,
|
|
62
|
+
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
57
63
|
splitSsmlDocument: () => splitSsmlDocument,
|
|
58
64
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
59
65
|
synthesizeSsml: () => synthesizeSsml,
|
|
60
66
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
67
|
+
synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
|
|
61
68
|
synthesizeSsmlSafe: () => synthesizeSsmlSafe,
|
|
62
69
|
validateAzureSsml: () => validateAzureSsml,
|
|
63
70
|
validateSsml: () => validateSsml,
|
|
@@ -1095,6 +1102,34 @@ function collectInheritedContext(nodes) {
|
|
|
1095
1102
|
nodes.forEach(visit);
|
|
1096
1103
|
return context;
|
|
1097
1104
|
}
|
|
1105
|
+
function elementName(node) {
|
|
1106
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1107
|
+
}
|
|
1108
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1109
|
+
let textOffset = 0;
|
|
1110
|
+
let firstPath;
|
|
1111
|
+
let foundPath;
|
|
1112
|
+
const visit = (node, path) => {
|
|
1113
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1114
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1115
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1116
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1117
|
+
textOffset += text.length;
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
node.children?.forEach((child, index) => {
|
|
1121
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1122
|
+
visit(child, childPath);
|
|
1123
|
+
});
|
|
1124
|
+
};
|
|
1125
|
+
nodes.forEach((node, index) => {
|
|
1126
|
+
if (!foundPath) {
|
|
1127
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1128
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
return foundPath ?? firstPath;
|
|
1132
|
+
}
|
|
1098
1133
|
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1099
1134
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1100
1135
|
const text = nodes.map(textFromNode).join("");
|
|
@@ -1110,7 +1145,8 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1110
1145
|
containedMarks: marks,
|
|
1111
1146
|
hasBackgroundAudio: chunkNodes.some(
|
|
1112
1147
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1113
|
-
)
|
|
1148
|
+
),
|
|
1149
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
1114
1150
|
};
|
|
1115
1151
|
}
|
|
1116
1152
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1336,7 +1372,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1336
1372
|
|
|
1337
1373
|
// packages/ssml-core/src/migration.ts
|
|
1338
1374
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1339
|
-
function
|
|
1375
|
+
function elementName2(element) {
|
|
1340
1376
|
switch (element.type) {
|
|
1341
1377
|
case "custom":
|
|
1342
1378
|
case "element":
|
|
@@ -1489,7 +1525,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1489
1525
|
}
|
|
1490
1526
|
return;
|
|
1491
1527
|
}
|
|
1492
|
-
const tag =
|
|
1528
|
+
const tag = elementName2(node);
|
|
1493
1529
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1494
1530
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1495
1531
|
});
|
|
@@ -1535,7 +1571,7 @@ function serializeDocument2(document) {
|
|
|
1535
1571
|
const serialize = (node) => {
|
|
1536
1572
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1537
1573
|
if (node.type === "text") return serialize(node.value);
|
|
1538
|
-
const tag =
|
|
1574
|
+
const tag = elementName2(node);
|
|
1539
1575
|
const nodeAttributes = elementAttributes(node);
|
|
1540
1576
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1541
1577
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1549,7 +1585,7 @@ function flatten(document) {
|
|
|
1549
1585
|
nodes.forEach((node, index) => {
|
|
1550
1586
|
if (typeof node === "string" || node.type === "text") return;
|
|
1551
1587
|
const currentPath = `${path}/${index}`;
|
|
1552
|
-
result.push({ name:
|
|
1588
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1553
1589
|
visit(childrenOf(node), currentPath);
|
|
1554
1590
|
});
|
|
1555
1591
|
};
|
|
@@ -1747,6 +1783,69 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1747
1783
|
];
|
|
1748
1784
|
|
|
1749
1785
|
// packages/ssml-core/src/azureValidation.ts
|
|
1786
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1787
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1788
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1789
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1790
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1791
|
+
const waiters = [];
|
|
1792
|
+
let active = 0;
|
|
1793
|
+
const acquire = async () => {
|
|
1794
|
+
if (active < concurrency) {
|
|
1795
|
+
active += 1;
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
1799
|
+
active += 1;
|
|
1800
|
+
};
|
|
1801
|
+
const release = () => {
|
|
1802
|
+
active -= 1;
|
|
1803
|
+
waiters.shift()?.();
|
|
1804
|
+
};
|
|
1805
|
+
const check = async (url, context) => {
|
|
1806
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1807
|
+
const cached = cache.get(url);
|
|
1808
|
+
if (cached !== void 0) return cached;
|
|
1809
|
+
const existing = inFlight.get(url);
|
|
1810
|
+
if (existing) return existing;
|
|
1811
|
+
const promise = (async () => {
|
|
1812
|
+
await acquire();
|
|
1813
|
+
try {
|
|
1814
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1815
|
+
const validation = Promise.resolve(validator(url, context));
|
|
1816
|
+
let timer;
|
|
1817
|
+
let abortHandler;
|
|
1818
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1819
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1820
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1821
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1822
|
+
timer = setTimeout(
|
|
1823
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1824
|
+
options.timeoutMs
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
try {
|
|
1829
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1830
|
+
cache.set(url, result);
|
|
1831
|
+
return result;
|
|
1832
|
+
} finally {
|
|
1833
|
+
if (timer) clearTimeout(timer);
|
|
1834
|
+
if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
|
|
1835
|
+
}
|
|
1836
|
+
} finally {
|
|
1837
|
+
release();
|
|
1838
|
+
}
|
|
1839
|
+
})();
|
|
1840
|
+
inFlight.set(url, promise);
|
|
1841
|
+
try {
|
|
1842
|
+
return await promise;
|
|
1843
|
+
} finally {
|
|
1844
|
+
inFlight.delete(url);
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
return (url, context) => check(url, context);
|
|
1848
|
+
}
|
|
1750
1849
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1751
1850
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1752
1851
|
"characters",
|
|
@@ -2041,23 +2140,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
2041
2140
|
);
|
|
2042
2141
|
}
|
|
2043
2142
|
}
|
|
2044
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2143
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
2045
2144
|
const src = attr(token, "src");
|
|
2046
2145
|
if (!src) {
|
|
2047
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2146
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
2048
2147
|
return;
|
|
2049
2148
|
}
|
|
2050
2149
|
let parsed;
|
|
2051
2150
|
try {
|
|
2052
2151
|
parsed = new URL(src);
|
|
2053
2152
|
} catch {
|
|
2054
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2153
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
2055
2154
|
return;
|
|
2056
2155
|
}
|
|
2057
2156
|
if (parsed.username || parsed.password)
|
|
2058
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2157
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
2059
2158
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2060
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2159
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2061
2160
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2062
2161
|
try {
|
|
2063
2162
|
const configured = new URL(allowedOrigin);
|
|
@@ -2069,13 +2168,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2069
2168
|
}
|
|
2070
2169
|
}) ?? false;
|
|
2071
2170
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2072
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2171
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2073
2172
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2074
2173
|
addDiagnostic(
|
|
2075
2174
|
diagnostics,
|
|
2076
2175
|
source,
|
|
2077
2176
|
token.start,
|
|
2078
|
-
`<${
|
|
2177
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2079
2178
|
);
|
|
2080
2179
|
}
|
|
2081
2180
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2393,6 +2492,14 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2393
2492
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2394
2493
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2395
2494
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2495
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2496
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2497
|
+
...runnerOptions,
|
|
2498
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2499
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2500
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2501
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2502
|
+
});
|
|
2396
2503
|
let tokens;
|
|
2397
2504
|
try {
|
|
2398
2505
|
tokens = tokenizeElements(ssml);
|
|
@@ -2402,7 +2509,7 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2402
2509
|
const checks = tokens.flatMap(
|
|
2403
2510
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2404
2511
|
try {
|
|
2405
|
-
const result = await
|
|
2512
|
+
const result = await boundedValidator(value, { tag: token.name, attribute });
|
|
2406
2513
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2407
2514
|
if (!valid) {
|
|
2408
2515
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2463,6 +2570,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
2463
2570
|
this.errorDetails = errorDetails;
|
|
2464
2571
|
}
|
|
2465
2572
|
};
|
|
2573
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
2574
|
+
constructor(format) {
|
|
2575
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
2576
|
+
this.name = "UnsupportedMergeFormatError";
|
|
2577
|
+
this.format = format;
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2466
2580
|
function createSpeechSdkError(error) {
|
|
2467
2581
|
const message = error instanceof Error ? error.message : String(error);
|
|
2468
2582
|
return new AzureTtsSdkError(message);
|
|
@@ -2540,6 +2654,149 @@ function createSpeechConfig(config) {
|
|
|
2540
2654
|
}
|
|
2541
2655
|
|
|
2542
2656
|
// packages/azure-tts-client/src/synthesis.ts
|
|
2657
|
+
function ascii(bytes, offset, value) {
|
|
2658
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
2659
|
+
}
|
|
2660
|
+
function readUint32(bytes, offset) {
|
|
2661
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
2662
|
+
}
|
|
2663
|
+
function parseWav(buffer) {
|
|
2664
|
+
const bytes = new Uint8Array(buffer);
|
|
2665
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
2666
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
2667
|
+
}
|
|
2668
|
+
const chunks = [];
|
|
2669
|
+
const dataParts = [];
|
|
2670
|
+
let format;
|
|
2671
|
+
let offset = 12;
|
|
2672
|
+
while (offset < bytes.byteLength) {
|
|
2673
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
2674
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
2675
|
+
const size = readUint32(bytes, offset + 4);
|
|
2676
|
+
const dataStart = offset + 8;
|
|
2677
|
+
const dataEnd = dataStart + size;
|
|
2678
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
2679
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
2680
|
+
chunks.push({ id, data: data2 });
|
|
2681
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
2682
|
+
if (id === "data") dataParts.push(data2);
|
|
2683
|
+
offset = dataEnd + (size & 1);
|
|
2684
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
2685
|
+
}
|
|
2686
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
2687
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
2688
|
+
const data = new Uint8Array(dataLength);
|
|
2689
|
+
let dataOffset = 0;
|
|
2690
|
+
for (const part of dataParts) {
|
|
2691
|
+
data.set(part, dataOffset);
|
|
2692
|
+
dataOffset += part.byteLength;
|
|
2693
|
+
}
|
|
2694
|
+
return { chunks, data, format };
|
|
2695
|
+
}
|
|
2696
|
+
function writeUint32(target, offset, value) {
|
|
2697
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
2698
|
+
}
|
|
2699
|
+
function writeChunk(target, offset, id, data) {
|
|
2700
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
2701
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
2702
|
+
target.set(data, offset + 8);
|
|
2703
|
+
const end = offset + 8 + data.byteLength;
|
|
2704
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
2705
|
+
return end + (data.byteLength & 1);
|
|
2706
|
+
}
|
|
2707
|
+
function mergeWavBuffers(buffers) {
|
|
2708
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
2709
|
+
const parsed = buffers.map(parseWav);
|
|
2710
|
+
const first = parsed[0];
|
|
2711
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
2712
|
+
if (parsed.some(
|
|
2713
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
2714
|
+
))
|
|
2715
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
2716
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
2717
|
+
const nonDataLength = first.chunks.reduce(
|
|
2718
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
2719
|
+
0
|
|
2720
|
+
);
|
|
2721
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
2722
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
2723
|
+
const output = new Uint8Array(outputLength);
|
|
2724
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
2725
|
+
writeUint32(output, 4, outputLength - 8);
|
|
2726
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
2727
|
+
let outputOffset = 12;
|
|
2728
|
+
let dataWritten = false;
|
|
2729
|
+
for (const chunk of first.chunks) {
|
|
2730
|
+
if (chunk.id === "data") {
|
|
2731
|
+
if (dataWritten) continue;
|
|
2732
|
+
const data = new Uint8Array(dataLength);
|
|
2733
|
+
let dataOffset = 0;
|
|
2734
|
+
for (const item of parsed) {
|
|
2735
|
+
data.set(item.data, dataOffset);
|
|
2736
|
+
dataOffset += item.data.byteLength;
|
|
2737
|
+
}
|
|
2738
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
2739
|
+
dataWritten = true;
|
|
2740
|
+
} else {
|
|
2741
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
2745
|
+
return output.buffer;
|
|
2746
|
+
}
|
|
2747
|
+
function skipId3v2(bytes) {
|
|
2748
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
2749
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
2750
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
2751
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
2752
|
+
}
|
|
2753
|
+
function stripMp3Tags(buffer) {
|
|
2754
|
+
const bytes = new Uint8Array(buffer);
|
|
2755
|
+
const start = skipId3v2(bytes);
|
|
2756
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
2757
|
+
return bytes.slice(Math.min(start, end), end);
|
|
2758
|
+
}
|
|
2759
|
+
function isMp3Format(format) {
|
|
2760
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
2761
|
+
}
|
|
2762
|
+
function isWavFormat(format) {
|
|
2763
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
2764
|
+
}
|
|
2765
|
+
function isRawFormat(format) {
|
|
2766
|
+
return /^raw(?:-|$)/i.test(format);
|
|
2767
|
+
}
|
|
2768
|
+
function resolveMergeAudioFormat(format) {
|
|
2769
|
+
if (isWavFormat(format)) return "wav";
|
|
2770
|
+
if (isMp3Format(format)) return "mp3";
|
|
2771
|
+
if (isRawFormat(format)) return "raw";
|
|
2772
|
+
return void 0;
|
|
2773
|
+
}
|
|
2774
|
+
function canMergeAudioFormat(format) {
|
|
2775
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
2776
|
+
}
|
|
2777
|
+
function mergeAudioBuffers(buffers, format) {
|
|
2778
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
2779
|
+
if (isMp3Format(format)) {
|
|
2780
|
+
const parts = buffers.map(stripMp3Tags);
|
|
2781
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
2782
|
+
let offset = 0;
|
|
2783
|
+
for (const part of parts) {
|
|
2784
|
+
output.set(part, offset);
|
|
2785
|
+
offset += part.byteLength;
|
|
2786
|
+
}
|
|
2787
|
+
return output.buffer;
|
|
2788
|
+
}
|
|
2789
|
+
if (isRawFormat(format)) {
|
|
2790
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
2791
|
+
let offset = 0;
|
|
2792
|
+
for (const buffer of buffers) {
|
|
2793
|
+
output.set(new Uint8Array(buffer), offset);
|
|
2794
|
+
offset += buffer.byteLength;
|
|
2795
|
+
}
|
|
2796
|
+
return output.buffer;
|
|
2797
|
+
}
|
|
2798
|
+
throw new UnsupportedMergeFormatError(format);
|
|
2799
|
+
}
|
|
2543
2800
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
2544
2801
|
try {
|
|
2545
2802
|
synthesizer.close();
|
|
@@ -2616,6 +2873,9 @@ async function synthesizeSsml(ssml, config) {
|
|
|
2616
2873
|
const addSourceMetadata = (event) => ({
|
|
2617
2874
|
...event,
|
|
2618
2875
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
2876
|
+
...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
2877
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
2878
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
2619
2879
|
...requestId ? { requestId } : {}
|
|
2620
2880
|
});
|
|
2621
2881
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -2651,60 +2911,126 @@ async function synthesizeSsml(ssml, config) {
|
|
|
2651
2911
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
2652
2912
|
const results = [];
|
|
2653
2913
|
const totalChunks = chunks.length;
|
|
2914
|
+
const report = (event) => config.onProgress?.(event);
|
|
2654
2915
|
for (const [index, chunk] of chunks.entries()) {
|
|
2655
2916
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2917
|
+
report({
|
|
2918
|
+
currentChunk: index,
|
|
2919
|
+
totalChunks,
|
|
2920
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
2921
|
+
chunkIndex: index,
|
|
2922
|
+
originalTextRange: input.originalTextRange,
|
|
2923
|
+
status: "pending",
|
|
2924
|
+
durationMs: 0
|
|
2660
2925
|
});
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2926
|
+
}
|
|
2927
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
2928
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
2929
|
+
report({
|
|
2930
|
+
currentChunk: index,
|
|
2664
2931
|
totalChunks,
|
|
2665
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
2932
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
2933
|
+
chunkIndex: index,
|
|
2934
|
+
originalTextRange: input.originalTextRange,
|
|
2935
|
+
status: "synthesizing",
|
|
2936
|
+
durationMs: 0
|
|
2666
2937
|
});
|
|
2938
|
+
const startedAt = Date.now();
|
|
2939
|
+
try {
|
|
2940
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
2941
|
+
...config,
|
|
2942
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
2943
|
+
...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
|
|
2944
|
+
chunkIndex: index,
|
|
2945
|
+
onProgress: void 0
|
|
2946
|
+
});
|
|
2947
|
+
results.push(result);
|
|
2948
|
+
report({
|
|
2949
|
+
currentChunk: index + 1,
|
|
2950
|
+
totalChunks,
|
|
2951
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
2952
|
+
chunkIndex: index,
|
|
2953
|
+
originalTextRange: input.originalTextRange,
|
|
2954
|
+
status: "success",
|
|
2955
|
+
durationMs: Date.now() - startedAt
|
|
2956
|
+
});
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
report({
|
|
2959
|
+
currentChunk: index,
|
|
2960
|
+
totalChunks,
|
|
2961
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
2962
|
+
chunkIndex: index,
|
|
2963
|
+
originalTextRange: input.originalTextRange,
|
|
2964
|
+
status: "failed",
|
|
2965
|
+
durationMs: Date.now() - startedAt,
|
|
2966
|
+
error
|
|
2967
|
+
});
|
|
2968
|
+
throw error;
|
|
2969
|
+
}
|
|
2667
2970
|
}
|
|
2668
|
-
return mergeSynthesisResults(results);
|
|
2971
|
+
return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
|
|
2669
2972
|
}
|
|
2670
|
-
function mergeSynthesisResults(results) {
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2973
|
+
function mergeSynthesisResults(results, format) {
|
|
2974
|
+
const audioData = format ? new Uint8Array(
|
|
2975
|
+
mergeAudioBuffers(
|
|
2976
|
+
results.map((result) => result.audioData),
|
|
2977
|
+
format
|
|
2978
|
+
)
|
|
2979
|
+
) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
|
|
2980
|
+
if (!format) {
|
|
2981
|
+
let offset = 0;
|
|
2982
|
+
for (const result of results) {
|
|
2983
|
+
audioData.set(new Uint8Array(result.audioData), offset);
|
|
2984
|
+
offset += result.audioData.byteLength;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2673
2987
|
const boundaries = [];
|
|
2674
2988
|
const visemes = [];
|
|
2675
2989
|
const bookmarks = [];
|
|
2676
|
-
let byteOffset = 0;
|
|
2677
2990
|
let durationOffset = 0;
|
|
2678
2991
|
for (const result of results) {
|
|
2679
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
2680
|
-
byteOffset += result.audioData.byteLength;
|
|
2681
2992
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
2682
2993
|
for (const boundary of chunkBoundaries) {
|
|
2683
2994
|
const textRange = boundary.textRange ?? result.textRange;
|
|
2995
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
2684
2996
|
const requestId = boundary.requestId ?? result.requestId;
|
|
2685
2997
|
boundaries.push({
|
|
2686
2998
|
...boundary,
|
|
2687
2999
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
3000
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
3001
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
3002
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
3003
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
2688
3004
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
2689
3005
|
...requestId ? { requestId } : {}
|
|
2690
3006
|
});
|
|
2691
3007
|
}
|
|
2692
3008
|
for (const viseme of result.visemes ?? []) {
|
|
2693
3009
|
const textRange = viseme.textRange ?? result.textRange;
|
|
3010
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
2694
3011
|
const requestId = viseme.requestId ?? result.requestId;
|
|
2695
3012
|
visemes.push({
|
|
2696
3013
|
...viseme,
|
|
2697
3014
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
3015
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
3016
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
3017
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
3018
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
2698
3019
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
2699
3020
|
...requestId ? { requestId } : {}
|
|
2700
3021
|
});
|
|
2701
3022
|
}
|
|
2702
3023
|
for (const bookmark of result.bookmarks ?? []) {
|
|
2703
3024
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
3025
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
2704
3026
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
2705
3027
|
bookmarks.push({
|
|
2706
3028
|
...bookmark,
|
|
2707
3029
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
3030
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
3031
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
3032
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
3033
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
2708
3034
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
2709
3035
|
...requestId ? { requestId } : {}
|
|
2710
3036
|
});
|
|
@@ -3496,6 +3822,69 @@ var AZURE_VOICE_DEFINITIONS2 = [
|
|
|
3496
3822
|
},
|
|
3497
3823
|
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
3498
3824
|
];
|
|
3825
|
+
function createAzureUrlValidatorRunner2(validator, options = {}) {
|
|
3826
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
3827
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
3828
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
3829
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
3830
|
+
const waiters = [];
|
|
3831
|
+
let active = 0;
|
|
3832
|
+
const acquire = async () => {
|
|
3833
|
+
if (active < concurrency) {
|
|
3834
|
+
active += 1;
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
3838
|
+
active += 1;
|
|
3839
|
+
};
|
|
3840
|
+
const release = () => {
|
|
3841
|
+
active -= 1;
|
|
3842
|
+
waiters.shift()?.();
|
|
3843
|
+
};
|
|
3844
|
+
const check = async (url, context) => {
|
|
3845
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
3846
|
+
const cached = cache.get(url);
|
|
3847
|
+
if (cached !== void 0) return cached;
|
|
3848
|
+
const existing = inFlight.get(url);
|
|
3849
|
+
if (existing) return existing;
|
|
3850
|
+
const promise = (async () => {
|
|
3851
|
+
await acquire();
|
|
3852
|
+
try {
|
|
3853
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
3854
|
+
const validation = Promise.resolve(validator(url, context));
|
|
3855
|
+
let timer;
|
|
3856
|
+
let abortHandler;
|
|
3857
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
3858
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
3859
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3860
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
3861
|
+
timer = setTimeout(
|
|
3862
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
3863
|
+
options.timeoutMs
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
try {
|
|
3868
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
3869
|
+
cache.set(url, result);
|
|
3870
|
+
return result;
|
|
3871
|
+
} finally {
|
|
3872
|
+
if (timer) clearTimeout(timer);
|
|
3873
|
+
if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
|
|
3874
|
+
}
|
|
3875
|
+
} finally {
|
|
3876
|
+
release();
|
|
3877
|
+
}
|
|
3878
|
+
})();
|
|
3879
|
+
inFlight.set(url, promise);
|
|
3880
|
+
try {
|
|
3881
|
+
return await promise;
|
|
3882
|
+
} finally {
|
|
3883
|
+
inFlight.delete(url);
|
|
3884
|
+
}
|
|
3885
|
+
};
|
|
3886
|
+
return (url, context) => check(url, context);
|
|
3887
|
+
}
|
|
3499
3888
|
var ALLOWED_BREAK_STRENGTHS2 = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
3500
3889
|
var ALLOWED_SAY_AS2 = /* @__PURE__ */ new Set([
|
|
3501
3890
|
"characters",
|
|
@@ -3780,23 +4169,23 @@ function validateVoiceFeatureMatrix2(token, source, diagnostics, voiceName, defi
|
|
|
3780
4169
|
);
|
|
3781
4170
|
}
|
|
3782
4171
|
}
|
|
3783
|
-
function validateAudioSource2(token, source, diagnostics, options,
|
|
4172
|
+
function validateAudioSource2(token, source, diagnostics, options, elementName3) {
|
|
3784
4173
|
const src = attr2(token, "src");
|
|
3785
4174
|
if (!src) {
|
|
3786
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4175
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
3787
4176
|
return;
|
|
3788
4177
|
}
|
|
3789
4178
|
let parsed;
|
|
3790
4179
|
try {
|
|
3791
4180
|
parsed = new URL(src);
|
|
3792
4181
|
} catch {
|
|
3793
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4182
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
3794
4183
|
return;
|
|
3795
4184
|
}
|
|
3796
4185
|
if (parsed.username || parsed.password)
|
|
3797
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4186
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
3798
4187
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
3799
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4188
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
3800
4189
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
3801
4190
|
try {
|
|
3802
4191
|
const configured = new URL(allowedOrigin);
|
|
@@ -3808,13 +4197,13 @@ function validateAudioSource2(token, source, diagnostics, options, elementName2)
|
|
|
3808
4197
|
}
|
|
3809
4198
|
}) ?? false;
|
|
3810
4199
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
3811
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4200
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
3812
4201
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
3813
4202
|
addDiagnostic2(
|
|
3814
4203
|
diagnostics,
|
|
3815
4204
|
source,
|
|
3816
4205
|
token.start,
|
|
3817
|
-
`<${
|
|
4206
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
3818
4207
|
);
|
|
3819
4208
|
}
|
|
3820
4209
|
function validateElement2(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -4132,6 +4521,14 @@ function validateAzureSsml2(ssml, options = {}) {
|
|
|
4132
4521
|
const diagnostics = validateAzureSsmlStatic2(ssml, options);
|
|
4133
4522
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
4134
4523
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
4524
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
4525
|
+
const boundedValidator = createAzureUrlValidatorRunner2(validator, {
|
|
4526
|
+
...runnerOptions,
|
|
4527
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
4528
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
4529
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
4530
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
4531
|
+
});
|
|
4135
4532
|
let tokens;
|
|
4136
4533
|
try {
|
|
4137
4534
|
tokens = tokenizeElements2(ssml);
|
|
@@ -4141,7 +4538,7 @@ function validateAzureSsml2(ssml, options = {}) {
|
|
|
4141
4538
|
const checks = tokens.flatMap(
|
|
4142
4539
|
(token) => urlAttributes2(token).map(async ({ attribute, value }) => {
|
|
4143
4540
|
try {
|
|
4144
|
-
const result = await
|
|
4541
|
+
const result = await boundedValidator(value, { tag: token.name, attribute });
|
|
4145
4542
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
4146
4543
|
if (!valid) {
|
|
4147
4544
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -4173,6 +4570,15 @@ var AZURE_VOICE_CATALOG_METADATA2 = {
|
|
|
4173
4570
|
};
|
|
4174
4571
|
|
|
4175
4572
|
// packages/azure-tts-client/src/safe.ts
|
|
4573
|
+
var ChunkValidationError = class extends Error {
|
|
4574
|
+
constructor(chunkIndex, diagnostics) {
|
|
4575
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
4576
|
+
this.kind = "chunk-validation";
|
|
4577
|
+
this.name = "ChunkValidationError";
|
|
4578
|
+
this.chunkIndex = chunkIndex;
|
|
4579
|
+
this.diagnostics = diagnostics;
|
|
4580
|
+
}
|
|
4581
|
+
};
|
|
4176
4582
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
4177
4583
|
const validationOptions = options.validation ?? options;
|
|
4178
4584
|
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
@@ -4196,6 +4602,95 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
4196
4602
|
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
4197
4603
|
}
|
|
4198
4604
|
}
|
|
4605
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
4606
|
+
const validationOptions = options.validation ?? options;
|
|
4607
|
+
const pending = (index, status, error) => {
|
|
4608
|
+
options.onProgress?.({
|
|
4609
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
4610
|
+
totalChunks: chunks.length,
|
|
4611
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
4612
|
+
chunkIndex: index,
|
|
4613
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
4614
|
+
status,
|
|
4615
|
+
durationMs: 0,
|
|
4616
|
+
...error ? { error } : {}
|
|
4617
|
+
});
|
|
4618
|
+
};
|
|
4619
|
+
chunks.forEach((_chunk, index) => {
|
|
4620
|
+
pending(index, "pending");
|
|
4621
|
+
});
|
|
4622
|
+
const validations = await Promise.all(
|
|
4623
|
+
chunks.map(async (chunk) => {
|
|
4624
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
4625
|
+
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
4626
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
4627
|
+
})
|
|
4628
|
+
);
|
|
4629
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
4630
|
+
if (firstInvalidIndex >= 0) {
|
|
4631
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
4632
|
+
pending(firstInvalidIndex, "failed", error);
|
|
4633
|
+
return { ok: false, success: false, status: "validation-error", error };
|
|
4634
|
+
}
|
|
4635
|
+
try {
|
|
4636
|
+
if (client.synthesizeChunks) {
|
|
4637
|
+
const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
|
|
4638
|
+
return { ok: true, success: true, status: "success", value };
|
|
4639
|
+
}
|
|
4640
|
+
const results = [];
|
|
4641
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
4642
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
4643
|
+
const sourceNodePath = input.sourceNodePath;
|
|
4644
|
+
pending(index, "synthesizing");
|
|
4645
|
+
const startedAt = Date.now();
|
|
4646
|
+
try {
|
|
4647
|
+
const result = await client.synthesizeSsml(input.ssml);
|
|
4648
|
+
results.push({
|
|
4649
|
+
...result,
|
|
4650
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
4651
|
+
...sourceNodePath ? {
|
|
4652
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
4653
|
+
...event,
|
|
4654
|
+
sourceNodePath: [...sourceNodePath]
|
|
4655
|
+
})),
|
|
4656
|
+
visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
4657
|
+
bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
|
|
4658
|
+
} : {}
|
|
4659
|
+
});
|
|
4660
|
+
options.onProgress?.({
|
|
4661
|
+
currentChunk: index + 1,
|
|
4662
|
+
totalChunks: chunks.length,
|
|
4663
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
4664
|
+
chunkIndex: index,
|
|
4665
|
+
originalTextRange: input.originalTextRange,
|
|
4666
|
+
status: "success",
|
|
4667
|
+
durationMs: Date.now() - startedAt
|
|
4668
|
+
});
|
|
4669
|
+
} catch (error) {
|
|
4670
|
+
options.onProgress?.({
|
|
4671
|
+
currentChunk: index,
|
|
4672
|
+
totalChunks: chunks.length,
|
|
4673
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
4674
|
+
chunkIndex: index,
|
|
4675
|
+
originalTextRange: input.originalTextRange,
|
|
4676
|
+
status: "failed",
|
|
4677
|
+
durationMs: Date.now() - startedAt,
|
|
4678
|
+
error
|
|
4679
|
+
});
|
|
4680
|
+
throw error;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return {
|
|
4684
|
+
ok: true,
|
|
4685
|
+
success: true,
|
|
4686
|
+
status: "success",
|
|
4687
|
+
value: mergeSynthesisResults(results, options.outputFormat)
|
|
4688
|
+
};
|
|
4689
|
+
} catch (error) {
|
|
4690
|
+
const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
|
|
4691
|
+
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4199
4694
|
|
|
4200
4695
|
// packages/azure-tts-client/src/client.ts
|
|
4201
4696
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -4234,6 +4729,16 @@ var AzureTtsClient = class {
|
|
|
4234
4729
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
4235
4730
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
4236
4731
|
}
|
|
4732
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
4733
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
4734
|
+
...options,
|
|
4735
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
4736
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
4740
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
4741
|
+
}
|
|
4237
4742
|
};
|
|
4238
4743
|
_options = new WeakMap();
|
|
4239
4744
|
|
|
@@ -4289,6 +4794,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4289
4794
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
4290
4795
|
const styles = stringList(record.StyleList);
|
|
4291
4796
|
const status = normalizeStatus(record.Status);
|
|
4797
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
4798
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
4799
|
+
const models = stringList(record.Models);
|
|
4292
4800
|
const merged = {
|
|
4293
4801
|
name: existing?.name ?? name,
|
|
4294
4802
|
locale: existing?.locale ?? locale,
|
|
@@ -4298,6 +4806,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4298
4806
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
4299
4807
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
4300
4808
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
4809
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
4810
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
4811
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
4812
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
4813
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
4814
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
4301
4815
|
if (status) merged.status = status;
|
|
4302
4816
|
else if (existing?.status) merged.status = existing.status;
|
|
4303
4817
|
voices.set(key, merged);
|
|
@@ -4319,9 +4833,13 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4319
4833
|
AzureTtsClient,
|
|
4320
4834
|
AzureTtsError,
|
|
4321
4835
|
AzureTtsSdkError,
|
|
4836
|
+
ChunkValidationError,
|
|
4837
|
+
UnsupportedMergeFormatError,
|
|
4322
4838
|
areAzureLanguagesEquivalent,
|
|
4323
4839
|
buildPartialSsml,
|
|
4324
4840
|
buildSsml,
|
|
4841
|
+
canMergeAudioFormat,
|
|
4842
|
+
createAzureUrlValidatorRunner,
|
|
4325
4843
|
extractSsmlText,
|
|
4326
4844
|
extractSsmlTranslatableText,
|
|
4327
4845
|
fetchAzureVoiceCatalog,
|
|
@@ -4330,13 +4848,16 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4330
4848
|
getBuiltInVoiceCatalogMetadata,
|
|
4331
4849
|
isValidAzureAudioDuration,
|
|
4332
4850
|
mapSsmlTextNodes,
|
|
4851
|
+
mergeAudioBuffers,
|
|
4333
4852
|
mergeSynthesisResults,
|
|
4334
4853
|
normalizeAzureLanguage,
|
|
4335
4854
|
parseSsml,
|
|
4855
|
+
resolveMergeAudioFormat,
|
|
4336
4856
|
splitSsmlDocument,
|
|
4337
4857
|
synthesizeSpeech,
|
|
4338
4858
|
synthesizeSsml,
|
|
4339
4859
|
synthesizeSsmlChunks,
|
|
4860
|
+
synthesizeSsmlChunksSafe,
|
|
4340
4861
|
synthesizeSsmlSafe,
|
|
4341
4862
|
validateAzureSsml,
|
|
4342
4863
|
validateSsml,
|