ssml-builder-js 2.13.0 → 2.15.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 +12 -2
- package/dist/{chunk-LCTE26LS.mjs → chunk-BDY2Q2JL.mjs} +2 -2
- package/dist/{chunk-25LOR4AJ.mjs → chunk-FXUM45ZY.mjs} +402 -169
- package/dist/chunk-FXUM45ZY.mjs.map +1 -0
- package/dist/{chunk-CZ2F3TET.mjs → chunk-WXFLUCLR.mjs} +227 -9
- package/dist/chunk-WXFLUCLR.mjs.map +1 -0
- package/dist/core.d.mts +65 -17
- package/dist/core.d.ts +65 -17
- package/dist/core.js +401 -166
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +5 -1
- package/dist/elements.js +101 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-Xbr6ZWnK.d.mts → index.d-8BvkB9gz.d.mts} +40 -2
- package/dist/{index.d-Xbr6ZWnK.d.ts → index.d-8BvkB9gz.d.ts} +40 -2
- package/dist/index.d.mts +170 -18
- package/dist/index.d.ts +170 -18
- package/dist/index.js +1512 -489
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +612 -47
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +15 -2
- package/dist/react.d.ts +15 -2
- package/dist/react.js +204 -23
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +105 -17
- 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-BDY2Q2JL.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -40,24 +40,37 @@ __export(src_exports, {
|
|
|
40
40
|
AzureTtsClient: () => AzureTtsClient,
|
|
41
41
|
AzureTtsError: () => AzureTtsError,
|
|
42
42
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
43
|
+
ChunkValidationError: () => ChunkValidationError,
|
|
44
|
+
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
45
|
+
MergeError: () => MergeError,
|
|
46
|
+
SynthesisCancelledError: () => SynthesisCancelledError,
|
|
47
|
+
SynthesisTimeoutError: () => SynthesisTimeoutError,
|
|
48
|
+
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
43
49
|
areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
|
|
44
50
|
buildPartialSsml: () => buildPartialSsml,
|
|
45
51
|
buildSsml: () => buildSsml,
|
|
52
|
+
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
53
|
+
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
46
54
|
extractSsmlText: () => extractSsmlText,
|
|
47
55
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
48
56
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
49
57
|
fromPlainTextToSsml: () => fromPlainTextToSsml,
|
|
50
58
|
getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
|
|
51
59
|
getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
|
|
60
|
+
getSsmlSourceMap: () => getSsmlSourceMap,
|
|
52
61
|
isValidAzureAudioDuration: () => isValidAzureAudioDuration,
|
|
53
62
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
63
|
+
mergeAudioBuffers: () => mergeAudioBuffers,
|
|
54
64
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
55
65
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
56
66
|
parseSsml: () => parseSsml,
|
|
67
|
+
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
68
|
+
resolveMimeType: () => resolveMimeType,
|
|
57
69
|
splitSsmlDocument: () => splitSsmlDocument,
|
|
58
70
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
59
71
|
synthesizeSsml: () => synthesizeSsml,
|
|
60
72
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
73
|
+
synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
|
|
61
74
|
synthesizeSsmlSafe: () => synthesizeSsmlSafe,
|
|
62
75
|
validateAzureSsml: () => validateAzureSsml,
|
|
63
76
|
validateSsml: () => validateSsml,
|
|
@@ -992,6 +1005,236 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
992
1005
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
993
1006
|
}
|
|
994
1007
|
|
|
1008
|
+
// packages/ssml-core/src/textNodes.ts
|
|
1009
|
+
function decodeXmlText(value) {
|
|
1010
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1011
|
+
if (entity === "&") return "&";
|
|
1012
|
+
if (entity === "'") return "'";
|
|
1013
|
+
if (entity === ">") return ">";
|
|
1014
|
+
if (entity === "<") return "<";
|
|
1015
|
+
if (entity === """) return '"';
|
|
1016
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1017
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1018
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
function encodeXmlText(value) {
|
|
1022
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1023
|
+
}
|
|
1024
|
+
function decodeXmlAttribute(value) {
|
|
1025
|
+
return decodeXmlText(value);
|
|
1026
|
+
}
|
|
1027
|
+
function findTagEnd(source, start) {
|
|
1028
|
+
let quote = "";
|
|
1029
|
+
for (let index = start; index < source.length; index += 1) {
|
|
1030
|
+
const character = source[index];
|
|
1031
|
+
if (quote) {
|
|
1032
|
+
if (character === quote) quote = "";
|
|
1033
|
+
} else if (character === '"' || character === "'") {
|
|
1034
|
+
quote = character;
|
|
1035
|
+
} else if (character === ">") {
|
|
1036
|
+
return index;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return source.length - 1;
|
|
1040
|
+
}
|
|
1041
|
+
function readTagName(tag) {
|
|
1042
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1043
|
+
return match?.[1];
|
|
1044
|
+
}
|
|
1045
|
+
function readTagAttributes(tag, name) {
|
|
1046
|
+
const attributes = {};
|
|
1047
|
+
const nameStart = tag.indexOf(name);
|
|
1048
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1049
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1050
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1051
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1052
|
+
}
|
|
1053
|
+
return attributes;
|
|
1054
|
+
}
|
|
1055
|
+
function collectTextNodes(source) {
|
|
1056
|
+
const nodes = [];
|
|
1057
|
+
const elements = [];
|
|
1058
|
+
let index = 0;
|
|
1059
|
+
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1060
|
+
if (!rawText) return;
|
|
1061
|
+
const path = elements.map((element) => element.name);
|
|
1062
|
+
const parent = elements[elements.length - 1];
|
|
1063
|
+
nodes.push({
|
|
1064
|
+
context: {
|
|
1065
|
+
ancestorTags: path.slice(0, -1),
|
|
1066
|
+
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1067
|
+
parentTag: parent?.name ?? "",
|
|
1068
|
+
path
|
|
1069
|
+
},
|
|
1070
|
+
decodedText: decodeXmlText(rawText),
|
|
1071
|
+
end,
|
|
1072
|
+
sourceEnd,
|
|
1073
|
+
sourceStart,
|
|
1074
|
+
start
|
|
1075
|
+
});
|
|
1076
|
+
};
|
|
1077
|
+
while (index < source.length) {
|
|
1078
|
+
if (source[index] !== "<") {
|
|
1079
|
+
const nextTag = source.indexOf("<", index);
|
|
1080
|
+
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1081
|
+
addText(index, end2, source.slice(index, end2));
|
|
1082
|
+
index = end2;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (source.startsWith("<!--", index)) {
|
|
1086
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1087
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1091
|
+
const contentStart = index + 9;
|
|
1092
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1093
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1094
|
+
addText(
|
|
1095
|
+
contentStart,
|
|
1096
|
+
contentEnd,
|
|
1097
|
+
source.slice(contentStart, contentEnd),
|
|
1098
|
+
index,
|
|
1099
|
+
end2 === -1 ? source.length : end2 + 3
|
|
1100
|
+
);
|
|
1101
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (source.startsWith("<?", index)) {
|
|
1105
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1106
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (source.startsWith("</", index)) {
|
|
1110
|
+
const end2 = findTagEnd(source, index + 2);
|
|
1111
|
+
elements.pop();
|
|
1112
|
+
index = end2 + 1;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const end = findTagEnd(source, index + 1);
|
|
1116
|
+
const tag = source.slice(index, end + 1);
|
|
1117
|
+
const name = readTagName(tag);
|
|
1118
|
+
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1119
|
+
index = end + 1;
|
|
1120
|
+
}
|
|
1121
|
+
return nodes;
|
|
1122
|
+
}
|
|
1123
|
+
function collectSourceMap(source) {
|
|
1124
|
+
const segments = [];
|
|
1125
|
+
const markers = [];
|
|
1126
|
+
const elements = [];
|
|
1127
|
+
let textOffset = 0;
|
|
1128
|
+
let index = 0;
|
|
1129
|
+
const textParts = [];
|
|
1130
|
+
const addText = (value) => {
|
|
1131
|
+
if (!value) return;
|
|
1132
|
+
const parent = elements[elements.length - 1];
|
|
1133
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1134
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
1135
|
+
const start = textOffset;
|
|
1136
|
+
textOffset += value.length;
|
|
1137
|
+
textParts.push(value);
|
|
1138
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
1139
|
+
};
|
|
1140
|
+
while (index < source.length) {
|
|
1141
|
+
if (source[index] !== "<") {
|
|
1142
|
+
const end2 = source.indexOf("<", index);
|
|
1143
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
1144
|
+
addText(decodeXmlText(source.slice(index, textEnd)));
|
|
1145
|
+
index = textEnd;
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (source.startsWith("<!--", index)) {
|
|
1149
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1150
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1154
|
+
const contentStart = index + 9;
|
|
1155
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1156
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1157
|
+
addText(source.slice(contentStart, contentEnd));
|
|
1158
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (source.startsWith("<?", index)) {
|
|
1162
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1163
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const end = findTagEnd(source, index + 1);
|
|
1167
|
+
const rawTag = source.slice(index, end + 1);
|
|
1168
|
+
if (rawTag.startsWith("</")) {
|
|
1169
|
+
elements.pop();
|
|
1170
|
+
index = end + 1;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const name = readTagName(rawTag);
|
|
1174
|
+
if (!name) {
|
|
1175
|
+
index = end + 1;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
const parent = elements[elements.length - 1];
|
|
1179
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
1180
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1181
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
1182
|
+
const attributes = readTagAttributes(rawTag, name);
|
|
1183
|
+
const normalizedName = name.toLowerCase();
|
|
1184
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
1185
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
1186
|
+
if (markerName) {
|
|
1187
|
+
markers.push({
|
|
1188
|
+
kind: normalizedName,
|
|
1189
|
+
name: markerName,
|
|
1190
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
1191
|
+
sourceNodePath: [...path]
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
1196
|
+
index = end + 1;
|
|
1197
|
+
}
|
|
1198
|
+
return { text: textParts.join(""), segments, markers };
|
|
1199
|
+
}
|
|
1200
|
+
function getSsmlSourceMap(ssml) {
|
|
1201
|
+
parseSsml(ssml);
|
|
1202
|
+
return collectSourceMap(ssml);
|
|
1203
|
+
}
|
|
1204
|
+
function extractSsmlText(ssml) {
|
|
1205
|
+
parseSsml(ssml);
|
|
1206
|
+
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1207
|
+
}
|
|
1208
|
+
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1209
|
+
parseSsml(ssml);
|
|
1210
|
+
const nodes = collectTextNodes(ssml);
|
|
1211
|
+
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1212
|
+
const replacements = await Promise.all(
|
|
1213
|
+
nodes.map(async (node) => {
|
|
1214
|
+
const context = {
|
|
1215
|
+
ancestorTags: [...node.context.ancestorTags],
|
|
1216
|
+
parentAttributes: { ...node.context.parentAttributes },
|
|
1217
|
+
parentTag: node.context.parentTag,
|
|
1218
|
+
path: [...node.context.path]
|
|
1219
|
+
};
|
|
1220
|
+
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1221
|
+
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1222
|
+
const transformed = await transform(node.decodedText, context);
|
|
1223
|
+
if (typeof transformed !== "string") {
|
|
1224
|
+
throw new TypeError("SSML text node transform must return a string");
|
|
1225
|
+
}
|
|
1226
|
+
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1227
|
+
})
|
|
1228
|
+
);
|
|
1229
|
+
let result = "";
|
|
1230
|
+
let cursor = 0;
|
|
1231
|
+
nodes.forEach((node, nodeIndex) => {
|
|
1232
|
+
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1233
|
+
cursor = node.sourceEnd;
|
|
1234
|
+
});
|
|
1235
|
+
return result + ssml.slice(cursor);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
995
1238
|
// packages/ssml-core/src/split.ts
|
|
996
1239
|
var DEFAULT_MAX_LENGTH = 1e4;
|
|
997
1240
|
function cloneElement(element, children) {
|
|
@@ -1095,7 +1338,35 @@ function collectInheritedContext(nodes) {
|
|
|
1095
1338
|
nodes.forEach(visit);
|
|
1096
1339
|
return context;
|
|
1097
1340
|
}
|
|
1098
|
-
function
|
|
1341
|
+
function elementName(node) {
|
|
1342
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1343
|
+
}
|
|
1344
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1345
|
+
let textOffset = 0;
|
|
1346
|
+
let firstPath;
|
|
1347
|
+
let foundPath;
|
|
1348
|
+
const visit = (node, path) => {
|
|
1349
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1350
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1351
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1352
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1353
|
+
textOffset += text.length;
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
node.children?.forEach((child, index) => {
|
|
1357
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1358
|
+
visit(child, childPath);
|
|
1359
|
+
});
|
|
1360
|
+
};
|
|
1361
|
+
nodes.forEach((node, index) => {
|
|
1362
|
+
if (!foundPath) {
|
|
1363
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1364
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
return foundPath ?? firstPath;
|
|
1368
|
+
}
|
|
1369
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1099
1370
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1100
1371
|
const text = nodes.map(textFromNode).join("");
|
|
1101
1372
|
const marks = [];
|
|
@@ -1110,7 +1381,24 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1110
1381
|
containedMarks: marks,
|
|
1111
1382
|
hasBackgroundAudio: chunkNodes.some(
|
|
1112
1383
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1113
|
-
)
|
|
1384
|
+
),
|
|
1385
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
|
|
1386
|
+
sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
|
|
1387
|
+
const start = Math.max(segment.range.start, textStart);
|
|
1388
|
+
const end = Math.min(segment.range.end, textStart + text.length);
|
|
1389
|
+
return {
|
|
1390
|
+
text: segment.text.slice(start - segment.range.start, end - segment.range.start),
|
|
1391
|
+
range: { start, end },
|
|
1392
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
1393
|
+
};
|
|
1394
|
+
}),
|
|
1395
|
+
sourceMarkers: sourceMap.markers.filter(
|
|
1396
|
+
({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
|
|
1397
|
+
).map((marker) => ({
|
|
1398
|
+
...marker,
|
|
1399
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
1400
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
1401
|
+
}))
|
|
1114
1402
|
};
|
|
1115
1403
|
}
|
|
1116
1404
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1120,11 +1408,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1120
1408
|
throw new RangeError("maxLength must be a positive integer");
|
|
1121
1409
|
}
|
|
1122
1410
|
const document = parseSsml(ssml);
|
|
1411
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1123
1412
|
const backgroundAudio = (document.children ?? []).find(
|
|
1124
1413
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1125
1414
|
);
|
|
1126
1415
|
if (ssml.length <= resolvedMaxLength) {
|
|
1127
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1416
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1128
1417
|
}
|
|
1129
1418
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1130
1419
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1148,7 +1437,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1148
1437
|
}
|
|
1149
1438
|
if (group.length > 0) chunks.push(group);
|
|
1150
1439
|
if (chunks.length === 0) {
|
|
1151
|
-
const result = createChunk(
|
|
1440
|
+
const result = createChunk(
|
|
1441
|
+
document,
|
|
1442
|
+
[],
|
|
1443
|
+
0,
|
|
1444
|
+
0,
|
|
1445
|
+
backgroundAudio,
|
|
1446
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1447
|
+
sourceMap,
|
|
1448
|
+
true
|
|
1449
|
+
);
|
|
1152
1450
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1153
1451
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1154
1452
|
}
|
|
@@ -1162,7 +1460,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1162
1460
|
chunkIndex,
|
|
1163
1461
|
textStart,
|
|
1164
1462
|
backgroundAudio,
|
|
1165
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1463
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1464
|
+
sourceMap,
|
|
1465
|
+
chunkIndex === chunks.length - 1
|
|
1166
1466
|
);
|
|
1167
1467
|
textStart = result.originalTextRange.end;
|
|
1168
1468
|
return result;
|
|
@@ -1185,172 +1485,23 @@ function validateSsml(xmlString) {
|
|
|
1185
1485
|
}
|
|
1186
1486
|
}
|
|
1187
1487
|
|
|
1188
|
-
// packages/ssml-core/src/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
return decodeXmlText(value);
|
|
1206
|
-
}
|
|
1207
|
-
function findTagEnd(source, start) {
|
|
1208
|
-
let quote = "";
|
|
1209
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1210
|
-
const character = source[index];
|
|
1211
|
-
if (quote) {
|
|
1212
|
-
if (character === quote) quote = "";
|
|
1213
|
-
} else if (character === '"' || character === "'") {
|
|
1214
|
-
quote = character;
|
|
1215
|
-
} else if (character === ">") {
|
|
1216
|
-
return index;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
return source.length - 1;
|
|
1220
|
-
}
|
|
1221
|
-
function readTagName(tag) {
|
|
1222
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1223
|
-
return match?.[1];
|
|
1224
|
-
}
|
|
1225
|
-
function readTagAttributes(tag, name) {
|
|
1226
|
-
const attributes = {};
|
|
1227
|
-
const nameStart = tag.indexOf(name);
|
|
1228
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1229
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1230
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1231
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1232
|
-
}
|
|
1233
|
-
return attributes;
|
|
1234
|
-
}
|
|
1235
|
-
function collectTextNodes(source) {
|
|
1236
|
-
const nodes = [];
|
|
1237
|
-
const elements = [];
|
|
1238
|
-
let index = 0;
|
|
1239
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1240
|
-
if (!rawText) return;
|
|
1241
|
-
const path = elements.map((element) => element.name);
|
|
1242
|
-
const parent = elements[elements.length - 1];
|
|
1243
|
-
nodes.push({
|
|
1244
|
-
context: {
|
|
1245
|
-
ancestorTags: path.slice(0, -1),
|
|
1246
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1247
|
-
parentTag: parent?.name ?? "",
|
|
1248
|
-
path
|
|
1249
|
-
},
|
|
1250
|
-
decodedText: decodeXmlText(rawText),
|
|
1251
|
-
end,
|
|
1252
|
-
sourceEnd,
|
|
1253
|
-
sourceStart,
|
|
1254
|
-
start
|
|
1255
|
-
});
|
|
1256
|
-
};
|
|
1257
|
-
while (index < source.length) {
|
|
1258
|
-
if (source[index] !== "<") {
|
|
1259
|
-
const nextTag = source.indexOf("<", index);
|
|
1260
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1261
|
-
addText(index, end2, source.slice(index, end2));
|
|
1262
|
-
index = end2;
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
if (source.startsWith("<!--", index)) {
|
|
1266
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1267
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1271
|
-
const contentStart = index + 9;
|
|
1272
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1273
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1274
|
-
addText(
|
|
1275
|
-
contentStart,
|
|
1276
|
-
contentEnd,
|
|
1277
|
-
source.slice(contentStart, contentEnd),
|
|
1278
|
-
index,
|
|
1279
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1280
|
-
);
|
|
1281
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
if (source.startsWith("<?", index)) {
|
|
1285
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1286
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (source.startsWith("</", index)) {
|
|
1290
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1291
|
-
elements.pop();
|
|
1292
|
-
index = end2 + 1;
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
const end = findTagEnd(source, index + 1);
|
|
1296
|
-
const tag = source.slice(index, end + 1);
|
|
1297
|
-
const name = readTagName(tag);
|
|
1298
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1299
|
-
index = end + 1;
|
|
1300
|
-
}
|
|
1301
|
-
return nodes;
|
|
1302
|
-
}
|
|
1303
|
-
function extractSsmlText(ssml) {
|
|
1304
|
-
parseSsml(ssml);
|
|
1305
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1306
|
-
}
|
|
1307
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1308
|
-
parseSsml(ssml);
|
|
1309
|
-
const nodes = collectTextNodes(ssml);
|
|
1310
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1311
|
-
const replacements = await Promise.all(
|
|
1312
|
-
nodes.map(async (node) => {
|
|
1313
|
-
const context = {
|
|
1314
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1315
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1316
|
-
parentTag: node.context.parentTag,
|
|
1317
|
-
path: [...node.context.path]
|
|
1318
|
-
};
|
|
1319
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1320
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1321
|
-
const transformed = await transform(node.decodedText, context);
|
|
1322
|
-
if (typeof transformed !== "string") {
|
|
1323
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1324
|
-
}
|
|
1325
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1326
|
-
})
|
|
1327
|
-
);
|
|
1328
|
-
let result = "";
|
|
1329
|
-
let cursor = 0;
|
|
1330
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1331
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1332
|
-
cursor = node.sourceEnd;
|
|
1333
|
-
});
|
|
1334
|
-
return result + ssml.slice(cursor);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// packages/ssml-core/src/migration.ts
|
|
1338
|
-
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1339
|
-
function elementName(element) {
|
|
1340
|
-
switch (element.type) {
|
|
1341
|
-
case "custom":
|
|
1342
|
-
case "element":
|
|
1343
|
-
return element.name;
|
|
1344
|
-
case "expressAs":
|
|
1345
|
-
return "mstts:express-as";
|
|
1346
|
-
case "sayAs":
|
|
1347
|
-
return "say-as";
|
|
1348
|
-
case "silence":
|
|
1349
|
-
return "mstts:silence";
|
|
1350
|
-
case "viseme":
|
|
1351
|
-
return "mstts:viseme";
|
|
1352
|
-
default:
|
|
1353
|
-
return element.type;
|
|
1488
|
+
// packages/ssml-core/src/migration.ts
|
|
1489
|
+
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1490
|
+
function elementName2(element) {
|
|
1491
|
+
switch (element.type) {
|
|
1492
|
+
case "custom":
|
|
1493
|
+
case "element":
|
|
1494
|
+
return element.name;
|
|
1495
|
+
case "expressAs":
|
|
1496
|
+
return "mstts:express-as";
|
|
1497
|
+
case "sayAs":
|
|
1498
|
+
return "say-as";
|
|
1499
|
+
case "silence":
|
|
1500
|
+
return "mstts:silence";
|
|
1501
|
+
case "viseme":
|
|
1502
|
+
return "mstts:viseme";
|
|
1503
|
+
default:
|
|
1504
|
+
return element.type;
|
|
1354
1505
|
}
|
|
1355
1506
|
}
|
|
1356
1507
|
function addAttribute2(attributes, name, value) {
|
|
@@ -1489,7 +1640,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1489
1640
|
}
|
|
1490
1641
|
return;
|
|
1491
1642
|
}
|
|
1492
|
-
const tag =
|
|
1643
|
+
const tag = elementName2(node);
|
|
1493
1644
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1494
1645
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1495
1646
|
});
|
|
@@ -1535,7 +1686,7 @@ function serializeDocument2(document) {
|
|
|
1535
1686
|
const serialize = (node) => {
|
|
1536
1687
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1537
1688
|
if (node.type === "text") return serialize(node.value);
|
|
1538
|
-
const tag =
|
|
1689
|
+
const tag = elementName2(node);
|
|
1539
1690
|
const nodeAttributes = elementAttributes(node);
|
|
1540
1691
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1541
1692
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1549,7 +1700,7 @@ function flatten(document) {
|
|
|
1549
1700
|
nodes.forEach((node, index) => {
|
|
1550
1701
|
if (typeof node === "string" || node.type === "text") return;
|
|
1551
1702
|
const currentPath = `${path}/${index}`;
|
|
1552
|
-
result.push({ name:
|
|
1703
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1553
1704
|
visit(childrenOf(node), currentPath);
|
|
1554
1705
|
});
|
|
1555
1706
|
};
|
|
@@ -1747,6 +1898,86 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1747
1898
|
];
|
|
1748
1899
|
|
|
1749
1900
|
// packages/ssml-core/src/azureValidation.ts
|
|
1901
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1902
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1903
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1904
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1905
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1906
|
+
const waiters = [];
|
|
1907
|
+
let active = 0;
|
|
1908
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1909
|
+
const acquire = async (signal) => {
|
|
1910
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1911
|
+
if (active < concurrency) {
|
|
1912
|
+
active += 1;
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
await new Promise((resolve, reject) => {
|
|
1916
|
+
let waiter;
|
|
1917
|
+
const abortHandler = () => {
|
|
1918
|
+
const index = waiters.indexOf(waiter);
|
|
1919
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
1920
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1921
|
+
reject(new Error("URL validation was aborted."));
|
|
1922
|
+
};
|
|
1923
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1924
|
+
waiter = () => {
|
|
1925
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1926
|
+
resolve();
|
|
1927
|
+
};
|
|
1928
|
+
waiters.push(waiter);
|
|
1929
|
+
});
|
|
1930
|
+
active += 1;
|
|
1931
|
+
};
|
|
1932
|
+
const release = () => {
|
|
1933
|
+
active -= 1;
|
|
1934
|
+
waiters.shift()?.();
|
|
1935
|
+
};
|
|
1936
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
1937
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1938
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
1939
|
+
const cached = cache.get(key);
|
|
1940
|
+
if (cached !== void 0) return cached;
|
|
1941
|
+
const existing = inFlight.get(key);
|
|
1942
|
+
if (existing) return existing;
|
|
1943
|
+
const promise = (async () => {
|
|
1944
|
+
await acquire(signal);
|
|
1945
|
+
try {
|
|
1946
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1947
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1948
|
+
let timer;
|
|
1949
|
+
let abortHandler;
|
|
1950
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1951
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1952
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1953
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1954
|
+
timer = setTimeout(
|
|
1955
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1956
|
+
options.timeoutMs
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
try {
|
|
1961
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1962
|
+
cache.set(key, result);
|
|
1963
|
+
return result;
|
|
1964
|
+
} finally {
|
|
1965
|
+
if (timer) clearTimeout(timer);
|
|
1966
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1967
|
+
}
|
|
1968
|
+
} finally {
|
|
1969
|
+
release();
|
|
1970
|
+
}
|
|
1971
|
+
})();
|
|
1972
|
+
inFlight.set(key, promise);
|
|
1973
|
+
try {
|
|
1974
|
+
return await promise;
|
|
1975
|
+
} finally {
|
|
1976
|
+
inFlight.delete(key);
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1980
|
+
}
|
|
1750
1981
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1751
1982
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1752
1983
|
"characters",
|
|
@@ -2041,23 +2272,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
2041
2272
|
);
|
|
2042
2273
|
}
|
|
2043
2274
|
}
|
|
2044
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2275
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
2045
2276
|
const src = attr(token, "src");
|
|
2046
2277
|
if (!src) {
|
|
2047
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2278
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
2048
2279
|
return;
|
|
2049
2280
|
}
|
|
2050
2281
|
let parsed;
|
|
2051
2282
|
try {
|
|
2052
2283
|
parsed = new URL(src);
|
|
2053
2284
|
} catch {
|
|
2054
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2285
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
2055
2286
|
return;
|
|
2056
2287
|
}
|
|
2057
2288
|
if (parsed.username || parsed.password)
|
|
2058
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2289
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
2059
2290
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
2060
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2291
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
2061
2292
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
2062
2293
|
try {
|
|
2063
2294
|
const configured = new URL(allowedOrigin);
|
|
@@ -2069,13 +2300,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
2069
2300
|
}
|
|
2070
2301
|
}) ?? false;
|
|
2071
2302
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
2072
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2303
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
2073
2304
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
2074
2305
|
addDiagnostic(
|
|
2075
2306
|
diagnostics,
|
|
2076
2307
|
source,
|
|
2077
2308
|
token.start,
|
|
2078
|
-
`<${
|
|
2309
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
2079
2310
|
);
|
|
2080
2311
|
}
|
|
2081
2312
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2393,6 +2624,15 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2393
2624
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2394
2625
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2395
2626
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2627
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2628
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2629
|
+
...runnerOptions,
|
|
2630
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2631
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2632
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2633
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2634
|
+
});
|
|
2635
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2396
2636
|
let tokens;
|
|
2397
2637
|
try {
|
|
2398
2638
|
tokens = tokenizeElements(ssml);
|
|
@@ -2402,7 +2642,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2402
2642
|
const checks = tokens.flatMap(
|
|
2403
2643
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2404
2644
|
try {
|
|
2405
|
-
const result = await
|
|
2645
|
+
const result = await boundedValidator(
|
|
2646
|
+
value,
|
|
2647
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2648
|
+
validationSignal
|
|
2649
|
+
);
|
|
2406
2650
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2407
2651
|
if (!valid) {
|
|
2408
2652
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2448,6 +2692,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2448
2692
|
var AzureTtsError = class extends Error {
|
|
2449
2693
|
constructor(status, statusText, responseBody, requestId) {
|
|
2450
2694
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
2695
|
+
this.kind = "azure-api-error";
|
|
2451
2696
|
this.name = "AzureTtsError";
|
|
2452
2697
|
this.status = status;
|
|
2453
2698
|
this.statusText = statusText;
|
|
@@ -2463,6 +2708,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
2463
2708
|
this.errorDetails = errorDetails;
|
|
2464
2709
|
}
|
|
2465
2710
|
};
|
|
2711
|
+
var SynthesisCancelledError = class extends Error {
|
|
2712
|
+
constructor(message = "Speech synthesis was cancelled.") {
|
|
2713
|
+
super(message);
|
|
2714
|
+
this.kind = "cancelled";
|
|
2715
|
+
this.name = "SynthesisCancelledError";
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
var SynthesisTimeoutError = class extends Error {
|
|
2719
|
+
constructor(message) {
|
|
2720
|
+
super(message);
|
|
2721
|
+
this.kind = "timeout";
|
|
2722
|
+
this.name = "SynthesisTimeoutError";
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
var MergeError = class extends Error {
|
|
2726
|
+
constructor(message, cause) {
|
|
2727
|
+
super(message);
|
|
2728
|
+
this.kind = "merge-error";
|
|
2729
|
+
this.name = "MergeError";
|
|
2730
|
+
this.cause = cause;
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
2734
|
+
constructor(format) {
|
|
2735
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
2736
|
+
this.kind = "unsupported-format-error";
|
|
2737
|
+
this.name = "UnsupportedMergeFormatError";
|
|
2738
|
+
this.format = format;
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
function toSynthesisError(error) {
|
|
2742
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
2743
|
+
return error;
|
|
2744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2745
|
+
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
2746
|
+
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
2747
|
+
return createSpeechSdkError(error);
|
|
2748
|
+
}
|
|
2466
2749
|
function createSpeechSdkError(error) {
|
|
2467
2750
|
const message = error instanceof Error ? error.message : String(error);
|
|
2468
2751
|
return new AzureTtsSdkError(message);
|
|
@@ -2471,263 +2754,9 @@ function createSpeechSdkError(error) {
|
|
|
2471
2754
|
// packages/azure-tts-client/src/synthesis.ts
|
|
2472
2755
|
var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
2473
2756
|
|
|
2474
|
-
// packages/
|
|
2475
|
-
var
|
|
2476
|
-
|
|
2477
|
-
// packages/azure-tts-client/src/outputFormats.ts
|
|
2478
|
-
var SpeechSDK = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
2479
|
-
var DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
|
|
2480
|
-
var OUTPUT_FORMATS = {
|
|
2481
|
-
"raw-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw,
|
|
2482
|
-
"riff-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren,
|
|
2483
|
-
"audio-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren,
|
|
2484
|
-
"audio-16khz-32kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3,
|
|
2485
|
-
"audio-16khz-128kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3,
|
|
2486
|
-
"audio-16khz-64kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz64KBitRateMonoMp3,
|
|
2487
|
-
"audio-24khz-48kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3,
|
|
2488
|
-
"audio-24khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3,
|
|
2489
|
-
"audio-24khz-160kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3,
|
|
2490
|
-
"raw-16khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoTrueSilk,
|
|
2491
|
-
"riff-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm,
|
|
2492
|
-
"riff-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz16BitMonoPcm,
|
|
2493
|
-
"riff-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm,
|
|
2494
|
-
"riff-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw,
|
|
2495
|
-
"raw-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm,
|
|
2496
|
-
"raw-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm,
|
|
2497
|
-
"raw-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm,
|
|
2498
|
-
"ogg-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg16Khz16BitMonoOpus,
|
|
2499
|
-
"ogg-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg24Khz16BitMonoOpus,
|
|
2500
|
-
"raw-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw48Khz16BitMonoPcm,
|
|
2501
|
-
"riff-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm,
|
|
2502
|
-
"audio-48khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz96KBitRateMonoMp3,
|
|
2503
|
-
"audio-48khz-192kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3,
|
|
2504
|
-
"ogg-48khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg48Khz16BitMonoOpus,
|
|
2505
|
-
"webm-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm16Khz16BitMonoOpus,
|
|
2506
|
-
"webm-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16BitMonoOpus,
|
|
2507
|
-
"webm-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16Bit24KbpsMonoOpus,
|
|
2508
|
-
"raw-24khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoTrueSilk,
|
|
2509
|
-
"raw-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoALaw,
|
|
2510
|
-
"riff-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoALaw,
|
|
2511
|
-
"audio-16khz-16bit-32kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16Bit32KbpsMonoOpus,
|
|
2512
|
-
"audio-24khz-16bit-48kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit48KbpsMonoOpus,
|
|
2513
|
-
"audio-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit24KbpsMonoOpus,
|
|
2514
|
-
"raw-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm,
|
|
2515
|
-
"riff-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff22050Hz16BitMonoPcm,
|
|
2516
|
-
"raw-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw44100Hz16BitMonoPcm,
|
|
2517
|
-
"riff-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm,
|
|
2518
|
-
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
2519
|
-
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
2520
|
-
};
|
|
2521
|
-
function resolveOutputFormat(outputFormat) {
|
|
2522
|
-
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
2523
|
-
if (resolvedFormat === void 0) {
|
|
2524
|
-
throw new Error(`Unsupported Azure Speech output format: ${outputFormat}`);
|
|
2525
|
-
}
|
|
2526
|
-
return resolvedFormat;
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
// packages/azure-tts-client/src/speechConfig.ts
|
|
2530
|
-
function resolveEndpoint(config) {
|
|
2531
|
-
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
2532
|
-
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
2533
|
-
}
|
|
2534
|
-
function createSpeechConfig(config) {
|
|
2535
|
-
const { outputFormat = DEFAULT_OUTPUT_FORMAT, subscriptionKey } = config;
|
|
2536
|
-
const endpoint = new URL(resolveEndpoint(config));
|
|
2537
|
-
const speechConfig = import_microsoft_cognitiveservices_speech_sdk.SpeechConfig.fromEndpoint(endpoint, subscriptionKey);
|
|
2538
|
-
speechConfig.speechSynthesisOutputFormat = resolveOutputFormat(outputFormat);
|
|
2539
|
-
return speechConfig;
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
// packages/azure-tts-client/src/synthesis.ts
|
|
2543
|
-
function closeSpeechResources(speechConfig, synthesizer) {
|
|
2544
|
-
try {
|
|
2545
|
-
synthesizer.close();
|
|
2546
|
-
} catch {
|
|
2547
|
-
}
|
|
2548
|
-
try {
|
|
2549
|
-
speechConfig.close();
|
|
2550
|
-
} catch {
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
2554
|
-
async function synthesizeSsml(ssml, config) {
|
|
2555
|
-
if (config.signal?.aborted) {
|
|
2556
|
-
throw createSpeechSdkError("Speech synthesis was cancelled.");
|
|
2557
|
-
}
|
|
2558
|
-
const speechConfig = createSpeechConfig(config);
|
|
2559
|
-
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
2560
|
-
return await new Promise((resolve, reject) => {
|
|
2561
|
-
let resourcesClosed = false;
|
|
2562
|
-
let settled = false;
|
|
2563
|
-
let timeout;
|
|
2564
|
-
let abortHandler;
|
|
2565
|
-
const cleanup = () => {
|
|
2566
|
-
if (timeout) clearTimeout(timeout);
|
|
2567
|
-
if (abortHandler) config.signal?.removeEventListener("abort", abortHandler);
|
|
2568
|
-
};
|
|
2569
|
-
const closeResources = () => {
|
|
2570
|
-
if (resourcesClosed) return;
|
|
2571
|
-
resourcesClosed = true;
|
|
2572
|
-
closeSpeechResources(speechConfig, synthesizer);
|
|
2573
|
-
};
|
|
2574
|
-
const rejectWithError = (error) => {
|
|
2575
|
-
if (settled) return;
|
|
2576
|
-
settled = true;
|
|
2577
|
-
cleanup();
|
|
2578
|
-
closeResources();
|
|
2579
|
-
reject(createSpeechSdkError(error));
|
|
2580
|
-
};
|
|
2581
|
-
const boundaries = [];
|
|
2582
|
-
const visemes = [];
|
|
2583
|
-
const bookmarks = [];
|
|
2584
|
-
synthesizer.wordBoundary = (_sender, event) => {
|
|
2585
|
-
boundaries.push({
|
|
2586
|
-
text: event.text,
|
|
2587
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
2588
|
-
durationMs: ticksToMilliseconds(event.duration)
|
|
2589
|
-
});
|
|
2590
|
-
};
|
|
2591
|
-
synthesizer.visemeReceived = (_sender, event) => {
|
|
2592
|
-
visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
|
|
2593
|
-
};
|
|
2594
|
-
synthesizer.bookmarkReached = (_sender, event) => {
|
|
2595
|
-
bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
|
|
2596
|
-
};
|
|
2597
|
-
const cb = (result) => {
|
|
2598
|
-
if (settled) return;
|
|
2599
|
-
const { reason, errorDetails } = result;
|
|
2600
|
-
if (reason !== SpeechSDK2.ResultReason.SynthesizingAudioCompleted) {
|
|
2601
|
-
const err = errorDetails || `Speech synthesis failed with reason ${reason}.`;
|
|
2602
|
-
rejectWithError(err);
|
|
2603
|
-
return;
|
|
2604
|
-
}
|
|
2605
|
-
settled = true;
|
|
2606
|
-
cleanup();
|
|
2607
|
-
closeResources();
|
|
2608
|
-
const eventDurationMs = Math.max(
|
|
2609
|
-
0,
|
|
2610
|
-
...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
|
|
2611
|
-
...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
|
|
2612
|
-
...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
|
|
2613
|
-
);
|
|
2614
|
-
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
2615
|
-
const requestId = result.resultId;
|
|
2616
|
-
const addSourceMetadata = (event) => ({
|
|
2617
|
-
...event,
|
|
2618
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
2619
|
-
...requestId ? { requestId } : {}
|
|
2620
|
-
});
|
|
2621
|
-
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
2622
|
-
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
2623
|
-
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
2624
|
-
resolve({
|
|
2625
|
-
audioData: result.audioData,
|
|
2626
|
-
durationMs,
|
|
2627
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
2628
|
-
...requestId ? { requestId } : {},
|
|
2629
|
-
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
2630
|
-
...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
|
|
2631
|
-
...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
|
|
2632
|
-
});
|
|
2633
|
-
};
|
|
2634
|
-
try {
|
|
2635
|
-
if (config.signal) {
|
|
2636
|
-
abortHandler = () => rejectWithError("Speech synthesis was cancelled.");
|
|
2637
|
-
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
2638
|
-
}
|
|
2639
|
-
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
2640
|
-
timeout = setTimeout(
|
|
2641
|
-
() => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
|
|
2642
|
-
config.timeoutMs
|
|
2643
|
-
);
|
|
2644
|
-
}
|
|
2645
|
-
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
2646
|
-
} catch (error) {
|
|
2647
|
-
rejectWithError(error);
|
|
2648
|
-
}
|
|
2649
|
-
});
|
|
2650
|
-
}
|
|
2651
|
-
async function synthesizeSsmlChunks(chunks, config) {
|
|
2652
|
-
const results = [];
|
|
2653
|
-
const totalChunks = chunks.length;
|
|
2654
|
-
for (const [index, chunk] of chunks.entries()) {
|
|
2655
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
2656
|
-
const result = await synthesizeSsml(input.ssml, {
|
|
2657
|
-
...config,
|
|
2658
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
2659
|
-
onProgress: void 0
|
|
2660
|
-
});
|
|
2661
|
-
results.push(result);
|
|
2662
|
-
config.onProgress?.({
|
|
2663
|
-
currentChunk: index + 1,
|
|
2664
|
-
totalChunks,
|
|
2665
|
-
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100)
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
2668
|
-
return mergeSynthesisResults(results);
|
|
2669
|
-
}
|
|
2670
|
-
function mergeSynthesisResults(results) {
|
|
2671
|
-
const audioLength = results.reduce((total, result) => total + result.audioData.byteLength, 0);
|
|
2672
|
-
const audioData = new Uint8Array(audioLength);
|
|
2673
|
-
const boundaries = [];
|
|
2674
|
-
const visemes = [];
|
|
2675
|
-
const bookmarks = [];
|
|
2676
|
-
let byteOffset = 0;
|
|
2677
|
-
let durationOffset = 0;
|
|
2678
|
-
for (const result of results) {
|
|
2679
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
2680
|
-
byteOffset += result.audioData.byteLength;
|
|
2681
|
-
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
2682
|
-
for (const boundary of chunkBoundaries) {
|
|
2683
|
-
const textRange = boundary.textRange ?? result.textRange;
|
|
2684
|
-
const requestId = boundary.requestId ?? result.requestId;
|
|
2685
|
-
boundaries.push({
|
|
2686
|
-
...boundary,
|
|
2687
|
-
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
2688
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
2689
|
-
...requestId ? { requestId } : {}
|
|
2690
|
-
});
|
|
2691
|
-
}
|
|
2692
|
-
for (const viseme of result.visemes ?? []) {
|
|
2693
|
-
const textRange = viseme.textRange ?? result.textRange;
|
|
2694
|
-
const requestId = viseme.requestId ?? result.requestId;
|
|
2695
|
-
visemes.push({
|
|
2696
|
-
...viseme,
|
|
2697
|
-
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
2698
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
2699
|
-
...requestId ? { requestId } : {}
|
|
2700
|
-
});
|
|
2701
|
-
}
|
|
2702
|
-
for (const bookmark of result.bookmarks ?? []) {
|
|
2703
|
-
const textRange = bookmark.textRange ?? result.textRange;
|
|
2704
|
-
const requestId = bookmark.requestId ?? result.requestId;
|
|
2705
|
-
bookmarks.push({
|
|
2706
|
-
...bookmark,
|
|
2707
|
-
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
2708
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
2709
|
-
...requestId ? { requestId } : {}
|
|
2710
|
-
});
|
|
2711
|
-
}
|
|
2712
|
-
durationOffset += Math.max(0, result.durationMs);
|
|
2713
|
-
}
|
|
2714
|
-
return {
|
|
2715
|
-
audioData: audioData.buffer,
|
|
2716
|
-
durationMs: durationOffset,
|
|
2717
|
-
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
2718
|
-
...visemes.length > 0 ? { visemes } : {},
|
|
2719
|
-
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
2720
|
-
...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
|
|
2721
|
-
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
async function synthesizeSpeech(ssml, config) {
|
|
2725
|
-
return (await synthesizeSsml(ssml, config)).audioData;
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
// packages/ssml-core/dist/index.mjs
|
|
2729
|
-
var __typeError2 = (msg) => {
|
|
2730
|
-
throw TypeError(msg);
|
|
2757
|
+
// packages/ssml-core/dist/index.mjs
|
|
2758
|
+
var __typeError2 = (msg) => {
|
|
2759
|
+
throw TypeError(msg);
|
|
2731
2760
|
};
|
|
2732
2761
|
var __accessCheck2 = (obj, member, msg) => member.has(obj) || __typeError2("Cannot " + msg);
|
|
2733
2762
|
var __privateGet2 = (obj, member, getter) => (__accessCheck2(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
@@ -3376,6 +3405,130 @@ function parseSsml2(xmlString) {
|
|
|
3376
3405
|
}
|
|
3377
3406
|
return document;
|
|
3378
3407
|
}
|
|
3408
|
+
function decodeXmlText2(value) {
|
|
3409
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
3410
|
+
if (entity === "&") return "&";
|
|
3411
|
+
if (entity === "'") return "'";
|
|
3412
|
+
if (entity === ">") return ">";
|
|
3413
|
+
if (entity === "<") return "<";
|
|
3414
|
+
if (entity === """) return '"';
|
|
3415
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
3416
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
3417
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
function decodeXmlAttribute2(value) {
|
|
3421
|
+
return decodeXmlText2(value);
|
|
3422
|
+
}
|
|
3423
|
+
function findTagEnd3(source, start) {
|
|
3424
|
+
let quote = "";
|
|
3425
|
+
for (let index = start; index < source.length; index += 1) {
|
|
3426
|
+
const character = source[index];
|
|
3427
|
+
if (quote) {
|
|
3428
|
+
if (character === quote) quote = "";
|
|
3429
|
+
} else if (character === '"' || character === "'") {
|
|
3430
|
+
quote = character;
|
|
3431
|
+
} else if (character === ">") {
|
|
3432
|
+
return index;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return source.length - 1;
|
|
3436
|
+
}
|
|
3437
|
+
function readTagName2(tag) {
|
|
3438
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
3439
|
+
return match?.[1];
|
|
3440
|
+
}
|
|
3441
|
+
function readTagAttributes2(tag, name) {
|
|
3442
|
+
const attributes = {};
|
|
3443
|
+
const nameStart = tag.indexOf(name);
|
|
3444
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
3445
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
3446
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
3447
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute2(match[3]);
|
|
3448
|
+
}
|
|
3449
|
+
return attributes;
|
|
3450
|
+
}
|
|
3451
|
+
function collectSourceMap2(source) {
|
|
3452
|
+
const segments = [];
|
|
3453
|
+
const markers = [];
|
|
3454
|
+
const elements = [];
|
|
3455
|
+
let textOffset = 0;
|
|
3456
|
+
let index = 0;
|
|
3457
|
+
const textParts = [];
|
|
3458
|
+
const addText = (value) => {
|
|
3459
|
+
if (!value) return;
|
|
3460
|
+
const parent = elements[elements.length - 1];
|
|
3461
|
+
if (parent) parent.nextChildIndex += 1;
|
|
3462
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
3463
|
+
const start = textOffset;
|
|
3464
|
+
textOffset += value.length;
|
|
3465
|
+
textParts.push(value);
|
|
3466
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
3467
|
+
};
|
|
3468
|
+
while (index < source.length) {
|
|
3469
|
+
if (source[index] !== "<") {
|
|
3470
|
+
const end2 = source.indexOf("<", index);
|
|
3471
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
3472
|
+
addText(decodeXmlText2(source.slice(index, textEnd)));
|
|
3473
|
+
index = textEnd;
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
if (source.startsWith("<!--", index)) {
|
|
3477
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
3478
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
3482
|
+
const contentStart = index + 9;
|
|
3483
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
3484
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
3485
|
+
addText(source.slice(contentStart, contentEnd));
|
|
3486
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (source.startsWith("<?", index)) {
|
|
3490
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
3491
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
const end = findTagEnd3(source, index + 1);
|
|
3495
|
+
const rawTag = source.slice(index, end + 1);
|
|
3496
|
+
if (rawTag.startsWith("</")) {
|
|
3497
|
+
elements.pop();
|
|
3498
|
+
index = end + 1;
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
const name = readTagName2(rawTag);
|
|
3502
|
+
if (!name) {
|
|
3503
|
+
index = end + 1;
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
const parent = elements[elements.length - 1];
|
|
3507
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
3508
|
+
if (parent) parent.nextChildIndex += 1;
|
|
3509
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
3510
|
+
const attributes = readTagAttributes2(rawTag, name);
|
|
3511
|
+
const normalizedName = name.toLowerCase();
|
|
3512
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
3513
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
3514
|
+
if (markerName) {
|
|
3515
|
+
markers.push({
|
|
3516
|
+
kind: normalizedName,
|
|
3517
|
+
name: markerName,
|
|
3518
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
3519
|
+
sourceNodePath: [...path]
|
|
3520
|
+
});
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
3524
|
+
index = end + 1;
|
|
3525
|
+
}
|
|
3526
|
+
return { text: textParts.join(""), segments, markers };
|
|
3527
|
+
}
|
|
3528
|
+
function getSsmlSourceMap2(ssml) {
|
|
3529
|
+
parseSsml2(ssml);
|
|
3530
|
+
return collectSourceMap2(ssml);
|
|
3531
|
+
}
|
|
3379
3532
|
var AZURE_VOICE_DEFINITIONS2 = [
|
|
3380
3533
|
{ name: "de-DE-ConradNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
3381
3534
|
{ name: "de-DE-KatjaNeural", locale: "de-DE", styles: ["cheerful", "sad"] },
|
|
@@ -3496,6 +3649,86 @@ var AZURE_VOICE_DEFINITIONS2 = [
|
|
|
3496
3649
|
},
|
|
3497
3650
|
{ name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
|
|
3498
3651
|
];
|
|
3652
|
+
function createAzureUrlValidatorRunner2(validator, options = {}) {
|
|
3653
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
3654
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
3655
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
3656
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
3657
|
+
const waiters = [];
|
|
3658
|
+
let active = 0;
|
|
3659
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
3660
|
+
const acquire = async (signal) => {
|
|
3661
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
3662
|
+
if (active < concurrency) {
|
|
3663
|
+
active += 1;
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
await new Promise((resolve, reject) => {
|
|
3667
|
+
let waiter;
|
|
3668
|
+
const abortHandler = () => {
|
|
3669
|
+
const index = waiters.indexOf(waiter);
|
|
3670
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
3671
|
+
signal.removeEventListener("abort", abortHandler);
|
|
3672
|
+
reject(new Error("URL validation was aborted."));
|
|
3673
|
+
};
|
|
3674
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
3675
|
+
waiter = () => {
|
|
3676
|
+
signal.removeEventListener("abort", abortHandler);
|
|
3677
|
+
resolve();
|
|
3678
|
+
};
|
|
3679
|
+
waiters.push(waiter);
|
|
3680
|
+
});
|
|
3681
|
+
active += 1;
|
|
3682
|
+
};
|
|
3683
|
+
const release = () => {
|
|
3684
|
+
active -= 1;
|
|
3685
|
+
waiters.shift()?.();
|
|
3686
|
+
};
|
|
3687
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
3688
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
3689
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
3690
|
+
const cached = cache.get(key);
|
|
3691
|
+
if (cached !== void 0) return cached;
|
|
3692
|
+
const existing = inFlight.get(key);
|
|
3693
|
+
if (existing) return existing;
|
|
3694
|
+
const promise = (async () => {
|
|
3695
|
+
await acquire(signal);
|
|
3696
|
+
try {
|
|
3697
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
3698
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
3699
|
+
let timer;
|
|
3700
|
+
let abortHandler;
|
|
3701
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
3702
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
3703
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
3704
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
3705
|
+
timer = setTimeout(
|
|
3706
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
3707
|
+
options.timeoutMs
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
try {
|
|
3712
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
3713
|
+
cache.set(key, result);
|
|
3714
|
+
return result;
|
|
3715
|
+
} finally {
|
|
3716
|
+
if (timer) clearTimeout(timer);
|
|
3717
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
3718
|
+
}
|
|
3719
|
+
} finally {
|
|
3720
|
+
release();
|
|
3721
|
+
}
|
|
3722
|
+
})();
|
|
3723
|
+
inFlight.set(key, promise);
|
|
3724
|
+
try {
|
|
3725
|
+
return await promise;
|
|
3726
|
+
} finally {
|
|
3727
|
+
inFlight.delete(key);
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
3731
|
+
}
|
|
3499
3732
|
var ALLOWED_BREAK_STRENGTHS2 = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
3500
3733
|
var ALLOWED_SAY_AS2 = /* @__PURE__ */ new Set([
|
|
3501
3734
|
"characters",
|
|
@@ -3780,23 +4013,23 @@ function validateVoiceFeatureMatrix2(token, source, diagnostics, voiceName, defi
|
|
|
3780
4013
|
);
|
|
3781
4014
|
}
|
|
3782
4015
|
}
|
|
3783
|
-
function validateAudioSource2(token, source, diagnostics, options,
|
|
4016
|
+
function validateAudioSource2(token, source, diagnostics, options, elementName3) {
|
|
3784
4017
|
const src = attr2(token, "src");
|
|
3785
4018
|
if (!src) {
|
|
3786
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4019
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
3787
4020
|
return;
|
|
3788
4021
|
}
|
|
3789
4022
|
let parsed;
|
|
3790
4023
|
try {
|
|
3791
4024
|
parsed = new URL(src);
|
|
3792
4025
|
} catch {
|
|
3793
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4026
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
3794
4027
|
return;
|
|
3795
4028
|
}
|
|
3796
4029
|
if (parsed.username || parsed.password)
|
|
3797
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4030
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
3798
4031
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
3799
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4032
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
3800
4033
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
3801
4034
|
try {
|
|
3802
4035
|
const configured = new URL(allowedOrigin);
|
|
@@ -3808,13 +4041,13 @@ function validateAudioSource2(token, source, diagnostics, options, elementName2)
|
|
|
3808
4041
|
}
|
|
3809
4042
|
}) ?? false;
|
|
3810
4043
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
3811
|
-
addDiagnostic2(diagnostics, source, token.start, `<${
|
|
4044
|
+
addDiagnostic2(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
3812
4045
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
3813
4046
|
addDiagnostic2(
|
|
3814
4047
|
diagnostics,
|
|
3815
4048
|
source,
|
|
3816
4049
|
token.start,
|
|
3817
|
-
`<${
|
|
4050
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
3818
4051
|
);
|
|
3819
4052
|
}
|
|
3820
4053
|
function validateElement2(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -4132,6 +4365,15 @@ function validateAzureSsml2(ssml, options = {}) {
|
|
|
4132
4365
|
const diagnostics = validateAzureSsmlStatic2(ssml, options);
|
|
4133
4366
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
4134
4367
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
4368
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
4369
|
+
const boundedValidator = createAzureUrlValidatorRunner2(validator, {
|
|
4370
|
+
...runnerOptions,
|
|
4371
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
4372
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
4373
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
4374
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
4375
|
+
});
|
|
4376
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
4135
4377
|
let tokens;
|
|
4136
4378
|
try {
|
|
4137
4379
|
tokens = tokenizeElements2(ssml);
|
|
@@ -4141,7 +4383,11 @@ function validateAzureSsml2(ssml, options = {}) {
|
|
|
4141
4383
|
const checks = tokens.flatMap(
|
|
4142
4384
|
(token) => urlAttributes2(token).map(async ({ attribute, value }) => {
|
|
4143
4385
|
try {
|
|
4144
|
-
const result = await
|
|
4386
|
+
const result = await boundedValidator(
|
|
4387
|
+
value,
|
|
4388
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
4389
|
+
validationSignal
|
|
4390
|
+
);
|
|
4145
4391
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
4146
4392
|
if (!valid) {
|
|
4147
4393
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -4172,51 +4418,793 @@ var AZURE_VOICE_CATALOG_METADATA2 = {
|
|
|
4172
4418
|
voiceCount: AZURE_VOICE_DEFINITIONS2.length
|
|
4173
4419
|
};
|
|
4174
4420
|
|
|
4175
|
-
// packages/azure-tts-client/src/
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4421
|
+
// packages/azure-tts-client/src/outputFormats.ts
|
|
4422
|
+
var SpeechSDK = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
4423
|
+
var DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
|
|
4424
|
+
var OUTPUT_FORMATS = {
|
|
4425
|
+
"raw-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw,
|
|
4426
|
+
"riff-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren,
|
|
4427
|
+
"audio-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren,
|
|
4428
|
+
"audio-16khz-32kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3,
|
|
4429
|
+
"audio-16khz-128kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3,
|
|
4430
|
+
"audio-16khz-64kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz64KBitRateMonoMp3,
|
|
4431
|
+
"audio-24khz-48kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3,
|
|
4432
|
+
"audio-24khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3,
|
|
4433
|
+
"audio-24khz-160kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3,
|
|
4434
|
+
"raw-16khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoTrueSilk,
|
|
4435
|
+
"riff-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm,
|
|
4436
|
+
"riff-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz16BitMonoPcm,
|
|
4437
|
+
"riff-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm,
|
|
4438
|
+
"riff-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw,
|
|
4439
|
+
"raw-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm,
|
|
4440
|
+
"raw-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm,
|
|
4441
|
+
"raw-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm,
|
|
4442
|
+
"ogg-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg16Khz16BitMonoOpus,
|
|
4443
|
+
"ogg-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg24Khz16BitMonoOpus,
|
|
4444
|
+
"raw-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw48Khz16BitMonoPcm,
|
|
4445
|
+
"riff-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm,
|
|
4446
|
+
"audio-48khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz96KBitRateMonoMp3,
|
|
4447
|
+
"audio-48khz-192kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3,
|
|
4448
|
+
"ogg-48khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg48Khz16BitMonoOpus,
|
|
4449
|
+
"webm-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm16Khz16BitMonoOpus,
|
|
4450
|
+
"webm-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16BitMonoOpus,
|
|
4451
|
+
"webm-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16Bit24KbpsMonoOpus,
|
|
4452
|
+
"raw-24khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoTrueSilk,
|
|
4453
|
+
"raw-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoALaw,
|
|
4454
|
+
"riff-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoALaw,
|
|
4455
|
+
"audio-16khz-16bit-32kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16Bit32KbpsMonoOpus,
|
|
4456
|
+
"audio-24khz-16bit-48kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit48KbpsMonoOpus,
|
|
4457
|
+
"audio-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit24KbpsMonoOpus,
|
|
4458
|
+
"raw-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm,
|
|
4459
|
+
"riff-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff22050Hz16BitMonoPcm,
|
|
4460
|
+
"raw-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw44100Hz16BitMonoPcm,
|
|
4461
|
+
"riff-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm,
|
|
4462
|
+
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
4463
|
+
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
4464
|
+
};
|
|
4465
|
+
function resolveMimeType(outputFormat) {
|
|
4466
|
+
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
4467
|
+
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
4468
|
+
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
4469
|
+
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
4470
|
+
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
4471
|
+
return "application/octet-stream";
|
|
4472
|
+
}
|
|
4473
|
+
function resolveOutputFormat(outputFormat) {
|
|
4474
|
+
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
4475
|
+
if (resolvedFormat === void 0) {
|
|
4476
|
+
throw new Error(`Unsupported Azure Speech output format: ${outputFormat}`);
|
|
4477
|
+
}
|
|
4478
|
+
return resolvedFormat;
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
// packages/azure-tts-client/src/speechConfig.ts
|
|
4482
|
+
var import_microsoft_cognitiveservices_speech_sdk = require("microsoft-cognitiveservices-speech-sdk");
|
|
4483
|
+
function resolveEndpoint(config) {
|
|
4484
|
+
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
4485
|
+
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
4486
|
+
}
|
|
4487
|
+
function createSpeechConfig(config) {
|
|
4488
|
+
const { outputFormat = DEFAULT_OUTPUT_FORMAT, subscriptionKey } = config;
|
|
4489
|
+
const endpoint = new URL(resolveEndpoint(config));
|
|
4490
|
+
const speechConfig = import_microsoft_cognitiveservices_speech_sdk.SpeechConfig.fromEndpoint(endpoint, subscriptionKey);
|
|
4491
|
+
speechConfig.speechSynthesisOutputFormat = resolveOutputFormat(outputFormat);
|
|
4492
|
+
return speechConfig;
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
// packages/azure-tts-client/src/synthesis.ts
|
|
4496
|
+
function ascii(bytes, offset, value) {
|
|
4497
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
4498
|
+
}
|
|
4499
|
+
function readUint32(bytes, offset) {
|
|
4500
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
4501
|
+
}
|
|
4502
|
+
function parseWav(buffer) {
|
|
4503
|
+
const bytes = new Uint8Array(buffer);
|
|
4504
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
4505
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
4506
|
+
}
|
|
4507
|
+
const chunks = [];
|
|
4508
|
+
const dataParts = [];
|
|
4509
|
+
let format;
|
|
4510
|
+
let offset = 12;
|
|
4511
|
+
while (offset < bytes.byteLength) {
|
|
4512
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
4513
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
4514
|
+
const size = readUint32(bytes, offset + 4);
|
|
4515
|
+
const dataStart = offset + 8;
|
|
4516
|
+
const dataEnd = dataStart + size;
|
|
4517
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
4518
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
4519
|
+
chunks.push({ id, data: data2 });
|
|
4520
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
4521
|
+
if (id === "data") dataParts.push(data2);
|
|
4522
|
+
offset = dataEnd + (size & 1);
|
|
4523
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
4524
|
+
}
|
|
4525
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
4526
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
4527
|
+
const data = new Uint8Array(dataLength);
|
|
4528
|
+
let dataOffset = 0;
|
|
4529
|
+
for (const part of dataParts) {
|
|
4530
|
+
data.set(part, dataOffset);
|
|
4531
|
+
dataOffset += part.byteLength;
|
|
4532
|
+
}
|
|
4533
|
+
return { chunks, data, format };
|
|
4534
|
+
}
|
|
4535
|
+
function writeUint32(target, offset, value) {
|
|
4536
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
4537
|
+
}
|
|
4538
|
+
function writeChunk(target, offset, id, data) {
|
|
4539
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
4540
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
4541
|
+
target.set(data, offset + 8);
|
|
4542
|
+
const end = offset + 8 + data.byteLength;
|
|
4543
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
4544
|
+
return end + (data.byteLength & 1);
|
|
4545
|
+
}
|
|
4546
|
+
function mergeWavBuffers(buffers) {
|
|
4547
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
4548
|
+
const parsed = buffers.map(parseWav);
|
|
4549
|
+
const first = parsed[0];
|
|
4550
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
4551
|
+
if (parsed.some(
|
|
4552
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
4553
|
+
))
|
|
4554
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
4555
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
4556
|
+
const nonDataLength = first.chunks.reduce(
|
|
4557
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
4558
|
+
0
|
|
4559
|
+
);
|
|
4560
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
4561
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
4562
|
+
const output = new Uint8Array(outputLength);
|
|
4563
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
4564
|
+
writeUint32(output, 4, outputLength - 8);
|
|
4565
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
4566
|
+
let outputOffset = 12;
|
|
4567
|
+
let dataWritten = false;
|
|
4568
|
+
for (const chunk of first.chunks) {
|
|
4569
|
+
if (chunk.id === "data") {
|
|
4570
|
+
if (dataWritten) continue;
|
|
4571
|
+
const data = new Uint8Array(dataLength);
|
|
4572
|
+
let dataOffset = 0;
|
|
4573
|
+
for (const item of parsed) {
|
|
4574
|
+
data.set(item.data, dataOffset);
|
|
4575
|
+
dataOffset += item.data.byteLength;
|
|
4576
|
+
}
|
|
4577
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
4578
|
+
dataWritten = true;
|
|
4579
|
+
} else {
|
|
4580
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
4584
|
+
return output.buffer;
|
|
4585
|
+
}
|
|
4586
|
+
function skipId3v2(bytes) {
|
|
4587
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
4588
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
4589
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
4590
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
4591
|
+
}
|
|
4592
|
+
function stripMp3Tags(buffer) {
|
|
4593
|
+
const bytes = new Uint8Array(buffer);
|
|
4594
|
+
const start = skipId3v2(bytes);
|
|
4595
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
4596
|
+
return bytes.slice(Math.min(start, end), end);
|
|
4597
|
+
}
|
|
4598
|
+
function isMp3Format(format) {
|
|
4599
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
4600
|
+
}
|
|
4601
|
+
function isWavFormat(format) {
|
|
4602
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
4603
|
+
}
|
|
4604
|
+
function isRawFormat(format) {
|
|
4605
|
+
return /^raw(?:-|$)/i.test(format);
|
|
4606
|
+
}
|
|
4607
|
+
function resolveMergeAudioFormat(format) {
|
|
4608
|
+
if (isWavFormat(format)) return "wav";
|
|
4609
|
+
if (isMp3Format(format)) return "mp3";
|
|
4610
|
+
if (isRawFormat(format)) return "raw";
|
|
4611
|
+
return void 0;
|
|
4612
|
+
}
|
|
4613
|
+
function canMergeAudioFormat(format) {
|
|
4614
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
4615
|
+
}
|
|
4616
|
+
function mergeAudioBuffers(buffers, options) {
|
|
4617
|
+
const format = typeof options === "string" ? options : options?.format;
|
|
4618
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
4619
|
+
try {
|
|
4620
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
4621
|
+
if (isMp3Format(format)) {
|
|
4622
|
+
const parts = buffers.map(stripMp3Tags);
|
|
4623
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
4624
|
+
let offset = 0;
|
|
4625
|
+
for (const part of parts) {
|
|
4626
|
+
output.set(part, offset);
|
|
4627
|
+
offset += part.byteLength;
|
|
4628
|
+
}
|
|
4629
|
+
return output.buffer;
|
|
4630
|
+
}
|
|
4631
|
+
if (isRawFormat(format)) {
|
|
4632
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
4633
|
+
let offset = 0;
|
|
4634
|
+
for (const buffer of buffers) {
|
|
4635
|
+
output.set(new Uint8Array(buffer), offset);
|
|
4636
|
+
offset += buffer.byteLength;
|
|
4637
|
+
}
|
|
4638
|
+
return output.buffer;
|
|
4639
|
+
}
|
|
4640
|
+
throw new UnsupportedMergeFormatError(format);
|
|
4641
|
+
} catch (error) {
|
|
4642
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
4643
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
function closeSpeechResources(speechConfig, synthesizer) {
|
|
4647
|
+
try {
|
|
4648
|
+
synthesizer.close();
|
|
4649
|
+
} catch {
|
|
4650
|
+
}
|
|
4651
|
+
try {
|
|
4652
|
+
speechConfig.close();
|
|
4653
|
+
} catch {
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
4657
|
+
async function synthesizeSsml(ssml, config) {
|
|
4658
|
+
if (config.signal?.aborted) {
|
|
4659
|
+
throw new SynthesisCancelledError();
|
|
4660
|
+
}
|
|
4661
|
+
const speechConfig = createSpeechConfig(config);
|
|
4662
|
+
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
4663
|
+
return await new Promise((resolve, reject) => {
|
|
4664
|
+
let resourcesClosed = false;
|
|
4665
|
+
let settled = false;
|
|
4666
|
+
let timeout;
|
|
4667
|
+
let abortHandler;
|
|
4668
|
+
const cleanup = () => {
|
|
4669
|
+
if (timeout) clearTimeout(timeout);
|
|
4670
|
+
if (abortHandler) config.signal?.removeEventListener("abort", abortHandler);
|
|
4671
|
+
};
|
|
4672
|
+
const closeResources = () => {
|
|
4673
|
+
if (resourcesClosed) return;
|
|
4674
|
+
resourcesClosed = true;
|
|
4675
|
+
closeSpeechResources(speechConfig, synthesizer);
|
|
4676
|
+
};
|
|
4677
|
+
const rejectWithError = (error) => {
|
|
4678
|
+
if (settled) return;
|
|
4679
|
+
settled = true;
|
|
4680
|
+
cleanup();
|
|
4681
|
+
closeResources();
|
|
4682
|
+
reject(toSynthesisError(error));
|
|
4683
|
+
};
|
|
4684
|
+
const boundaries = [];
|
|
4685
|
+
const visemes = [];
|
|
4686
|
+
const bookmarks = [];
|
|
4687
|
+
let sourceEventCursor = 0;
|
|
4688
|
+
let generatedSourceMap;
|
|
4689
|
+
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
4690
|
+
try {
|
|
4691
|
+
generatedSourceMap = getSsmlSourceMap2(ssml);
|
|
4692
|
+
} catch {
|
|
4693
|
+
generatedSourceMap = void 0;
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
4697
|
+
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
4698
|
+
...segment,
|
|
4699
|
+
range: {
|
|
4700
|
+
start: segment.range.start + sourceBaseOffset,
|
|
4701
|
+
end: segment.range.end + sourceBaseOffset
|
|
4702
|
+
},
|
|
4703
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
4704
|
+
})) ?? [];
|
|
4705
|
+
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
4706
|
+
...marker,
|
|
4707
|
+
originalTextRange: {
|
|
4708
|
+
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
4709
|
+
end: marker.originalTextRange.end + sourceBaseOffset
|
|
4710
|
+
},
|
|
4711
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
4712
|
+
})) ?? [];
|
|
4713
|
+
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
4714
|
+
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
4715
|
+
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
4716
|
+
if (marker) {
|
|
4717
|
+
return {
|
|
4718
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
4719
|
+
sourceNodePath: [...marker.sourceNodePath],
|
|
4720
|
+
textRange: { ...marker.originalTextRange }
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
|
|
4724
|
+
const value = text ?? "";
|
|
4725
|
+
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
4726
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
|
|
4727
|
+
localStart = -1;
|
|
4728
|
+
if (localStart < 0 || localStart > sourceText.length) {
|
|
4729
|
+
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
4730
|
+
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
4731
|
+
}
|
|
4732
|
+
localStart = Math.max(0, localStart);
|
|
4733
|
+
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
4734
|
+
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
4735
|
+
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
4736
|
+
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
4737
|
+
const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
|
|
4738
|
+
return {
|
|
4739
|
+
originalTextRange: { ...fallbackRange },
|
|
4740
|
+
textRange: { ...fallbackRange },
|
|
4741
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
4742
|
+
};
|
|
4743
|
+
};
|
|
4744
|
+
synthesizer.wordBoundary = (_sender, event) => {
|
|
4745
|
+
boundaries.push({
|
|
4746
|
+
text: event.text,
|
|
4747
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
4748
|
+
durationMs: ticksToMilliseconds(event.duration),
|
|
4749
|
+
...mapSourceEvent(
|
|
4750
|
+
event.text,
|
|
4751
|
+
event.textOffset
|
|
4752
|
+
)
|
|
4753
|
+
});
|
|
4754
|
+
};
|
|
4755
|
+
synthesizer.visemeReceived = (_sender, event) => {
|
|
4756
|
+
const eventWithOffset = event;
|
|
4757
|
+
visemes.push({
|
|
4758
|
+
visemeId: event.visemeId,
|
|
4759
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
4760
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
4761
|
+
});
|
|
4762
|
+
};
|
|
4763
|
+
synthesizer.bookmarkReached = (_sender, event) => {
|
|
4764
|
+
const eventWithOffset = event;
|
|
4765
|
+
bookmarks.push({
|
|
4766
|
+
name: event.text,
|
|
4767
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
4768
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
4769
|
+
});
|
|
4770
|
+
};
|
|
4771
|
+
const cb = (result) => {
|
|
4772
|
+
if (settled) return;
|
|
4773
|
+
const { reason, errorDetails } = result;
|
|
4774
|
+
if (reason !== SpeechSDK2.ResultReason.SynthesizingAudioCompleted) {
|
|
4775
|
+
const err = errorDetails || `Speech synthesis failed with reason ${reason}.`;
|
|
4776
|
+
rejectWithError(err);
|
|
4777
|
+
return;
|
|
4778
|
+
}
|
|
4779
|
+
settled = true;
|
|
4780
|
+
cleanup();
|
|
4781
|
+
closeResources();
|
|
4782
|
+
const eventDurationMs = Math.max(
|
|
4783
|
+
0,
|
|
4784
|
+
...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
|
|
4785
|
+
...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
|
|
4786
|
+
...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
|
|
4787
|
+
);
|
|
4788
|
+
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
4789
|
+
const requestId = result.resultId;
|
|
4790
|
+
const addSourceMetadata = (event) => ({
|
|
4791
|
+
...event,
|
|
4792
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
4793
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
4794
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
4795
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
4796
|
+
...requestId ? { requestId } : {}
|
|
4797
|
+
});
|
|
4798
|
+
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
4799
|
+
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
4800
|
+
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
4801
|
+
resolve({
|
|
4802
|
+
audioData: result.audioData,
|
|
4803
|
+
durationMs,
|
|
4804
|
+
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
4805
|
+
...requestId ? { requestId } : {},
|
|
4806
|
+
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
4807
|
+
...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
|
|
4808
|
+
...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
|
|
4809
|
+
});
|
|
4810
|
+
};
|
|
4811
|
+
try {
|
|
4812
|
+
if (config.signal) {
|
|
4813
|
+
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
4814
|
+
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
4815
|
+
}
|
|
4816
|
+
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
4817
|
+
timeout = setTimeout(
|
|
4818
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
|
|
4819
|
+
config.timeoutMs
|
|
4820
|
+
);
|
|
4821
|
+
}
|
|
4822
|
+
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
4823
|
+
} catch (error) {
|
|
4824
|
+
rejectWithError(error);
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
async function synthesizeSsmlChunks(chunks, config) {
|
|
4829
|
+
const results = [];
|
|
4830
|
+
const totalChunks = chunks.length;
|
|
4831
|
+
const report = (event) => config.onProgress?.(event);
|
|
4832
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
4833
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
4834
|
+
report({
|
|
4835
|
+
currentChunk: index,
|
|
4836
|
+
totalChunks,
|
|
4837
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
4838
|
+
chunkIndex: index,
|
|
4839
|
+
originalTextRange: input.originalTextRange,
|
|
4840
|
+
status: "pending",
|
|
4841
|
+
durationMs: 0
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4844
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
4845
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
4846
|
+
report({
|
|
4847
|
+
currentChunk: index,
|
|
4848
|
+
totalChunks,
|
|
4849
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
4850
|
+
chunkIndex: index,
|
|
4851
|
+
originalTextRange: input.originalTextRange,
|
|
4852
|
+
status: "synthesizing",
|
|
4853
|
+
durationMs: 0
|
|
4854
|
+
});
|
|
4855
|
+
const startedAt = Date.now();
|
|
4856
|
+
try {
|
|
4857
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
4858
|
+
...config,
|
|
4859
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
4860
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
4861
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
4862
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
4863
|
+
chunkIndex: index,
|
|
4864
|
+
onProgress: void 0
|
|
4865
|
+
});
|
|
4866
|
+
results.push(result);
|
|
4867
|
+
report({
|
|
4868
|
+
currentChunk: index + 1,
|
|
4869
|
+
totalChunks,
|
|
4870
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
4871
|
+
chunkIndex: index,
|
|
4872
|
+
originalTextRange: input.originalTextRange,
|
|
4873
|
+
status: "success",
|
|
4874
|
+
durationMs: Date.now() - startedAt
|
|
4875
|
+
});
|
|
4876
|
+
} catch (error) {
|
|
4877
|
+
report({
|
|
4878
|
+
currentChunk: index,
|
|
4879
|
+
totalChunks,
|
|
4880
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
4881
|
+
chunkIndex: index,
|
|
4882
|
+
originalTextRange: input.originalTextRange,
|
|
4883
|
+
status: "failed",
|
|
4884
|
+
durationMs: Date.now() - startedAt,
|
|
4885
|
+
error
|
|
4886
|
+
});
|
|
4887
|
+
throw error;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
return mergeSynthesisResults(results, {
|
|
4891
|
+
format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
function createMergedResult(results, audioData, format) {
|
|
4895
|
+
const boundaries = [];
|
|
4896
|
+
const visemes = [];
|
|
4897
|
+
const bookmarks = [];
|
|
4898
|
+
let durationOffset = 0;
|
|
4899
|
+
for (const [resultIndex, result] of results.entries()) {
|
|
4900
|
+
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
4901
|
+
for (const boundary of chunkBoundaries) {
|
|
4902
|
+
const textRange = boundary.textRange ?? result.textRange;
|
|
4903
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
4904
|
+
const requestId = boundary.requestId ?? result.requestId;
|
|
4905
|
+
boundaries.push({
|
|
4906
|
+
...boundary,
|
|
4907
|
+
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
4908
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
4909
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
4910
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
4911
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
4912
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
4913
|
+
...requestId ? { requestId } : {}
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
for (const viseme of result.visemes ?? []) {
|
|
4917
|
+
const textRange = viseme.textRange ?? result.textRange;
|
|
4918
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
4919
|
+
const requestId = viseme.requestId ?? result.requestId;
|
|
4920
|
+
visemes.push({
|
|
4921
|
+
...viseme,
|
|
4922
|
+
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
4923
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
4924
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
4925
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
4926
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
4927
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
4928
|
+
...requestId ? { requestId } : {}
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
for (const bookmark of result.bookmarks ?? []) {
|
|
4932
|
+
const textRange = bookmark.textRange ?? result.textRange;
|
|
4933
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
4934
|
+
const requestId = bookmark.requestId ?? result.requestId;
|
|
4935
|
+
bookmarks.push({
|
|
4936
|
+
...bookmark,
|
|
4937
|
+
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
4938
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
4939
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
4940
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
4941
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
4942
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
4943
|
+
...requestId ? { requestId } : {}
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
durationOffset += Math.max(0, result.durationMs);
|
|
4947
|
+
}
|
|
4948
|
+
return {
|
|
4949
|
+
audioData,
|
|
4950
|
+
durationMs: durationOffset,
|
|
4951
|
+
mimeType: resolveMimeType(format),
|
|
4952
|
+
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
4953
|
+
...visemes.length > 0 ? { visemes } : {},
|
|
4954
|
+
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
4955
|
+
...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
|
|
4956
|
+
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4959
|
+
function mergeSynthesisResults(results, options) {
|
|
4960
|
+
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
4961
|
+
const format = resolvedOptions?.format;
|
|
4962
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
4963
|
+
const buffers = results.map((result) => result.audioData);
|
|
4964
|
+
if (resolvedOptions.customMerger) {
|
|
4965
|
+
return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
|
|
4966
|
+
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
4967
|
+
return createMergedResult(results, merged, format);
|
|
4968
|
+
}).catch((error) => {
|
|
4969
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
4970
|
+
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
try {
|
|
4974
|
+
return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
|
|
4975
|
+
} catch (error) {
|
|
4976
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
4977
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
async function synthesizeSpeech(ssml, config) {
|
|
4981
|
+
return (await synthesizeSsml(ssml, config)).audioData;
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
// packages/azure-tts-client/src/safe.ts
|
|
4985
|
+
var ChunkValidationError = class extends Error {
|
|
4986
|
+
constructor(chunkIndex, diagnostics) {
|
|
4987
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
4988
|
+
this.kind = "validation-error";
|
|
4989
|
+
this.name = "ChunkValidationError";
|
|
4990
|
+
this.chunkIndex = chunkIndex;
|
|
4991
|
+
this.diagnostics = diagnostics;
|
|
4992
|
+
}
|
|
4993
|
+
};
|
|
4994
|
+
function failure(error) {
|
|
4995
|
+
return { ok: false, success: false, status: error.kind, error };
|
|
4996
|
+
}
|
|
4997
|
+
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
4998
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
4999
|
+
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
5000
|
+
if (options.signal?.aborted) {
|
|
5001
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
5002
|
+
return failure(error);
|
|
5003
|
+
}
|
|
5004
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
5005
|
+
if (errors.length > 0) {
|
|
5006
|
+
return failure({
|
|
5007
|
+
kind: "validation-error",
|
|
5008
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
5009
|
+
diagnostics: errors
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
try {
|
|
5013
|
+
return {
|
|
5014
|
+
ok: true,
|
|
5015
|
+
success: true,
|
|
5016
|
+
status: "success",
|
|
5017
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal })
|
|
5018
|
+
};
|
|
5019
|
+
} catch (error) {
|
|
5020
|
+
const synthesisError = toSynthesisError(error);
|
|
5021
|
+
return failure(synthesisError);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
5025
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
5026
|
+
if (options.signal?.aborted) {
|
|
5027
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
5028
|
+
return failure(error);
|
|
5029
|
+
}
|
|
5030
|
+
const pending = (index, status, error) => {
|
|
5031
|
+
options.onProgress?.({
|
|
5032
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
5033
|
+
totalChunks: chunks.length,
|
|
5034
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
5035
|
+
chunkIndex: index,
|
|
5036
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
5037
|
+
status,
|
|
5038
|
+
durationMs: 0,
|
|
5039
|
+
...error ? { error } : {}
|
|
5040
|
+
});
|
|
5041
|
+
};
|
|
5042
|
+
chunks.forEach((_chunk, index) => {
|
|
5043
|
+
pending(index, "pending");
|
|
5044
|
+
});
|
|
5045
|
+
const validations = await Promise.all(
|
|
5046
|
+
chunks.map(async (chunk) => {
|
|
5047
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
5048
|
+
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
5049
|
+
const diagnostics = await Promise.resolve(
|
|
5050
|
+
validateAzureSsml2(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
|
|
5051
|
+
);
|
|
5052
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
5053
|
+
})
|
|
5054
|
+
);
|
|
5055
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
5056
|
+
if (firstInvalidIndex >= 0) {
|
|
5057
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
5058
|
+
pending(firstInvalidIndex, "failed", error);
|
|
5059
|
+
return failure(error);
|
|
5060
|
+
}
|
|
5061
|
+
try {
|
|
5062
|
+
if (client.synthesizeChunks) {
|
|
5063
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
5064
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
5065
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
5066
|
+
});
|
|
5067
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
5068
|
+
onProgress: options.onProgress,
|
|
5069
|
+
outputFormat: options.outputFormat,
|
|
5070
|
+
signal: options.signal,
|
|
5071
|
+
timeoutMs: options.timeoutMs,
|
|
5072
|
+
sourceNodePath: options.sourceNodePath
|
|
5073
|
+
});
|
|
5074
|
+
return { ok: true, success: true, status: "success", value };
|
|
5075
|
+
}
|
|
5076
|
+
const results = [];
|
|
5077
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
5078
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5079
|
+
const sourceNodePath = input.sourceNodePath;
|
|
5080
|
+
const originalTextRange = input.originalTextRange;
|
|
5081
|
+
pending(index, "synthesizing");
|
|
5082
|
+
const startedAt = Date.now();
|
|
5083
|
+
try {
|
|
5084
|
+
const result = await client.synthesizeSsml(input.ssml, {
|
|
5085
|
+
outputFormat: options.outputFormat,
|
|
5086
|
+
signal: options.signal,
|
|
5087
|
+
timeoutMs: options.timeoutMs,
|
|
5088
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
5089
|
+
});
|
|
5090
|
+
results.push({
|
|
5091
|
+
...result,
|
|
5092
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
5093
|
+
...sourceNodePath ? {
|
|
5094
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
5095
|
+
...event,
|
|
5096
|
+
sourceNodePath: [...sourceNodePath],
|
|
5097
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
5098
|
+
})),
|
|
5099
|
+
visemes: result.visemes?.map((event) => ({
|
|
5100
|
+
...event,
|
|
5101
|
+
sourceNodePath: [...sourceNodePath],
|
|
5102
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
5103
|
+
})),
|
|
5104
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
5105
|
+
...event,
|
|
5106
|
+
sourceNodePath: [...sourceNodePath],
|
|
5107
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
5108
|
+
}))
|
|
5109
|
+
} : {},
|
|
5110
|
+
...originalTextRange ? {
|
|
5111
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
5112
|
+
...event,
|
|
5113
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
5114
|
+
})),
|
|
5115
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
5116
|
+
...event,
|
|
5117
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
5118
|
+
})),
|
|
5119
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
5120
|
+
...event,
|
|
5121
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
5122
|
+
})),
|
|
5123
|
+
visemes: result.visemes?.map((event) => ({
|
|
5124
|
+
...event,
|
|
5125
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
5126
|
+
})),
|
|
5127
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
5128
|
+
...event,
|
|
5129
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
5130
|
+
}))
|
|
5131
|
+
} : {}
|
|
5132
|
+
});
|
|
5133
|
+
options.onProgress?.({
|
|
5134
|
+
currentChunk: index + 1,
|
|
5135
|
+
totalChunks: chunks.length,
|
|
5136
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
5137
|
+
chunkIndex: index,
|
|
5138
|
+
originalTextRange: input.originalTextRange,
|
|
5139
|
+
status: "success",
|
|
5140
|
+
durationMs: Date.now() - startedAt
|
|
5141
|
+
});
|
|
5142
|
+
} catch (error) {
|
|
5143
|
+
options.onProgress?.({
|
|
5144
|
+
currentChunk: index,
|
|
5145
|
+
totalChunks: chunks.length,
|
|
5146
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
5147
|
+
chunkIndex: index,
|
|
5148
|
+
originalTextRange: input.originalTextRange,
|
|
5149
|
+
status: "failed",
|
|
5150
|
+
durationMs: Date.now() - startedAt,
|
|
5151
|
+
error
|
|
5152
|
+
});
|
|
5153
|
+
throw error;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
return {
|
|
5157
|
+
ok: true,
|
|
5158
|
+
success: true,
|
|
5159
|
+
status: "success",
|
|
5160
|
+
value: mergeSynthesisResults(results, {
|
|
5161
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
5162
|
+
})
|
|
5163
|
+
};
|
|
5164
|
+
} catch (error) {
|
|
5165
|
+
const synthesisError = toSynthesisError(error);
|
|
5166
|
+
return failure(synthesisError);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
function withValidationSignal(options, signal) {
|
|
5170
|
+
if (!signal) return options;
|
|
5171
|
+
return {
|
|
5172
|
+
...options,
|
|
5173
|
+
urlValidatorSignal: signal,
|
|
5174
|
+
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
5175
|
+
};
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
// packages/azure-tts-client/src/client.ts
|
|
5179
|
+
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
5180
|
+
var _options;
|
|
5181
|
+
var AzureTtsClient = class {
|
|
5182
|
+
constructor(options) {
|
|
5183
|
+
__privateAdd(this, _options);
|
|
5184
|
+
__privateSet(this, _options, options);
|
|
5185
|
+
}
|
|
5186
|
+
async synthesize(ssml) {
|
|
5187
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
5188
|
+
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
5189
|
+
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
4212
5190
|
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
4213
5191
|
return synthesizeSpeech(ssml, config);
|
|
4214
5192
|
}
|
|
4215
|
-
async synthesizeSsml(ssml) {
|
|
5193
|
+
async synthesizeSsml(ssml, options = {}) {
|
|
4216
5194
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
4217
5195
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
4218
5196
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
4219
|
-
return synthesizeSsml(ssml, {
|
|
5197
|
+
return synthesizeSsml(ssml, {
|
|
5198
|
+
endpoint,
|
|
5199
|
+
region,
|
|
5200
|
+
subscriptionKey,
|
|
5201
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
5202
|
+
signal: options.signal ?? signal,
|
|
5203
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
5204
|
+
sourceNodePath: options.sourceNodePath,
|
|
5205
|
+
sourceTextSegments: options.sourceTextSegments,
|
|
5206
|
+
sourceMarkers: options.sourceMarkers
|
|
5207
|
+
});
|
|
4220
5208
|
}
|
|
4221
5209
|
async synthesizeChunks(chunks, options = {}) {
|
|
4222
5210
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
@@ -4225,15 +5213,28 @@ var AzureTtsClient = class {
|
|
|
4225
5213
|
endpoint,
|
|
4226
5214
|
region,
|
|
4227
5215
|
subscriptionKey,
|
|
4228
|
-
outputFormat,
|
|
4229
|
-
signal,
|
|
4230
|
-
timeoutMs,
|
|
5216
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
5217
|
+
signal: options.signal ?? signal,
|
|
5218
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
5219
|
+
sourceNodePath: options.sourceNodePath,
|
|
4231
5220
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
4232
5221
|
});
|
|
4233
5222
|
}
|
|
4234
5223
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
4235
5224
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
4236
5225
|
}
|
|
5226
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
5227
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
5228
|
+
...options,
|
|
5229
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
5230
|
+
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
5231
|
+
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
5232
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
5236
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
5237
|
+
}
|
|
4237
5238
|
};
|
|
4238
5239
|
_options = new WeakMap();
|
|
4239
5240
|
|
|
@@ -4289,6 +5290,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4289
5290
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
4290
5291
|
const styles = stringList(record.StyleList);
|
|
4291
5292
|
const status = normalizeStatus(record.Status);
|
|
5293
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
5294
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
5295
|
+
const models = stringList(record.Models);
|
|
4292
5296
|
const merged = {
|
|
4293
5297
|
name: existing?.name ?? name,
|
|
4294
5298
|
locale: existing?.locale ?? locale,
|
|
@@ -4298,6 +5302,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4298
5302
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
4299
5303
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
4300
5304
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
5305
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
5306
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
5307
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
5308
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
5309
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
5310
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
4301
5311
|
if (status) merged.status = status;
|
|
4302
5312
|
else if (existing?.status) merged.status = existing.status;
|
|
4303
5313
|
voices.set(key, merged);
|
|
@@ -4319,24 +5329,37 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
4319
5329
|
AzureTtsClient,
|
|
4320
5330
|
AzureTtsError,
|
|
4321
5331
|
AzureTtsSdkError,
|
|
5332
|
+
ChunkValidationError,
|
|
5333
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
5334
|
+
MergeError,
|
|
5335
|
+
SynthesisCancelledError,
|
|
5336
|
+
SynthesisTimeoutError,
|
|
5337
|
+
UnsupportedMergeFormatError,
|
|
4322
5338
|
areAzureLanguagesEquivalent,
|
|
4323
5339
|
buildPartialSsml,
|
|
4324
5340
|
buildSsml,
|
|
5341
|
+
canMergeAudioFormat,
|
|
5342
|
+
createAzureUrlValidatorRunner,
|
|
4325
5343
|
extractSsmlText,
|
|
4326
5344
|
extractSsmlTranslatableText,
|
|
4327
5345
|
fetchAzureVoiceCatalog,
|
|
4328
5346
|
fromPlainTextToSsml,
|
|
4329
5347
|
getAzureVoiceCatalogMetadata,
|
|
4330
5348
|
getBuiltInVoiceCatalogMetadata,
|
|
5349
|
+
getSsmlSourceMap,
|
|
4331
5350
|
isValidAzureAudioDuration,
|
|
4332
5351
|
mapSsmlTextNodes,
|
|
5352
|
+
mergeAudioBuffers,
|
|
4333
5353
|
mergeSynthesisResults,
|
|
4334
5354
|
normalizeAzureLanguage,
|
|
4335
5355
|
parseSsml,
|
|
5356
|
+
resolveMergeAudioFormat,
|
|
5357
|
+
resolveMimeType,
|
|
4336
5358
|
splitSsmlDocument,
|
|
4337
5359
|
synthesizeSpeech,
|
|
4338
5360
|
synthesizeSsml,
|
|
4339
5361
|
synthesizeSsmlChunks,
|
|
5362
|
+
synthesizeSsmlChunksSafe,
|
|
4340
5363
|
synthesizeSsmlSafe,
|
|
4341
5364
|
validateAzureSsml,
|
|
4342
5365
|
validateSsml,
|