ssml-builder-js 2.3.0 → 2.5.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 +158 -0
- package/dist/{chunk-PSN6XAFF.mjs → chunk-4RHHW33A.mjs} +551 -2
- package/dist/chunk-4RHHW33A.mjs.map +1 -0
- package/dist/{chunk-NPZ44CHU.mjs → chunk-ALFYHSCR.mjs} +1 -1
- package/dist/chunk-ALFYHSCR.mjs.map +1 -0
- package/dist/core.d.mts +36 -1
- package/dist/core.d.ts +36 -1
- package/dist/core.js +552 -0
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +7 -1
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +1 -1
- package/dist/index.d.mts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +584 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +39 -6
- package/dist/index.mjs.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/package.json +12 -3
- package/dist/chunk-NPZ44CHU.mjs.map +0 -1
- package/dist/chunk-PSN6XAFF.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -42,8 +42,11 @@ __export(src_exports, {
|
|
|
42
42
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
43
43
|
buildPartialSsml: () => buildPartialSsml,
|
|
44
44
|
buildSsml: () => buildSsml,
|
|
45
|
+
extractSsmlText: () => extractSsmlText,
|
|
46
|
+
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
45
47
|
parseSsml: () => parseSsml,
|
|
46
48
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
49
|
+
validateAzureSsml: () => validateAzureSsml,
|
|
47
50
|
validateSsml: () => validateSsml
|
|
48
51
|
});
|
|
49
52
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -892,6 +895,552 @@ function validateSsml(xmlString) {
|
|
|
892
895
|
}
|
|
893
896
|
}
|
|
894
897
|
|
|
898
|
+
// packages/ssml-core/src/textNodes.ts
|
|
899
|
+
function decodeXmlText(value) {
|
|
900
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
901
|
+
if (entity === "&") return "&";
|
|
902
|
+
if (entity === "'") return "'";
|
|
903
|
+
if (entity === ">") return ">";
|
|
904
|
+
if (entity === "<") return "<";
|
|
905
|
+
if (entity === """) return '"';
|
|
906
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
907
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
908
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
function encodeXmlText(value) {
|
|
912
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
913
|
+
}
|
|
914
|
+
function decodeXmlAttribute(value) {
|
|
915
|
+
return decodeXmlText(value);
|
|
916
|
+
}
|
|
917
|
+
function findTagEnd(source, start) {
|
|
918
|
+
let quote = "";
|
|
919
|
+
for (let index = start; index < source.length; index += 1) {
|
|
920
|
+
const character = source[index];
|
|
921
|
+
if (quote) {
|
|
922
|
+
if (character === quote) quote = "";
|
|
923
|
+
} else if (character === '"' || character === "'") {
|
|
924
|
+
quote = character;
|
|
925
|
+
} else if (character === ">") {
|
|
926
|
+
return index;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return source.length - 1;
|
|
930
|
+
}
|
|
931
|
+
function readTagName(tag) {
|
|
932
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
933
|
+
return match?.[1];
|
|
934
|
+
}
|
|
935
|
+
function readTagAttributes(tag, name) {
|
|
936
|
+
const attributes = {};
|
|
937
|
+
const nameStart = tag.indexOf(name);
|
|
938
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
939
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
940
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
941
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
942
|
+
}
|
|
943
|
+
return attributes;
|
|
944
|
+
}
|
|
945
|
+
function collectTextNodes(source) {
|
|
946
|
+
const nodes = [];
|
|
947
|
+
const elements = [];
|
|
948
|
+
let index = 0;
|
|
949
|
+
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
950
|
+
if (!rawText) return;
|
|
951
|
+
const path = elements.map((element) => element.name);
|
|
952
|
+
const parent = elements[elements.length - 1];
|
|
953
|
+
nodes.push({
|
|
954
|
+
context: {
|
|
955
|
+
ancestorTags: path.slice(0, -1),
|
|
956
|
+
parentAttributes: { ...parent?.attributes ?? {} },
|
|
957
|
+
parentTag: parent?.name ?? "",
|
|
958
|
+
path
|
|
959
|
+
},
|
|
960
|
+
decodedText: decodeXmlText(rawText),
|
|
961
|
+
end,
|
|
962
|
+
sourceEnd,
|
|
963
|
+
sourceStart,
|
|
964
|
+
start
|
|
965
|
+
});
|
|
966
|
+
};
|
|
967
|
+
while (index < source.length) {
|
|
968
|
+
if (source[index] !== "<") {
|
|
969
|
+
const nextTag = source.indexOf("<", index);
|
|
970
|
+
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
971
|
+
addText(index, end2, source.slice(index, end2));
|
|
972
|
+
index = end2;
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (source.startsWith("<!--", index)) {
|
|
976
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
977
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
981
|
+
const contentStart = index + 9;
|
|
982
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
983
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
984
|
+
addText(
|
|
985
|
+
contentStart,
|
|
986
|
+
contentEnd,
|
|
987
|
+
source.slice(contentStart, contentEnd),
|
|
988
|
+
index,
|
|
989
|
+
end2 === -1 ? source.length : end2 + 3
|
|
990
|
+
);
|
|
991
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (source.startsWith("<?", index)) {
|
|
995
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
996
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (source.startsWith("</", index)) {
|
|
1000
|
+
const end2 = findTagEnd(source, index + 2);
|
|
1001
|
+
elements.pop();
|
|
1002
|
+
index = end2 + 1;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const end = findTagEnd(source, index + 1);
|
|
1006
|
+
const tag = source.slice(index, end + 1);
|
|
1007
|
+
const name = readTagName(tag);
|
|
1008
|
+
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1009
|
+
index = end + 1;
|
|
1010
|
+
}
|
|
1011
|
+
return nodes;
|
|
1012
|
+
}
|
|
1013
|
+
function extractSsmlText(ssml) {
|
|
1014
|
+
parseSsml(ssml);
|
|
1015
|
+
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1016
|
+
}
|
|
1017
|
+
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1018
|
+
parseSsml(ssml);
|
|
1019
|
+
const nodes = collectTextNodes(ssml);
|
|
1020
|
+
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1021
|
+
const replacements = await Promise.all(
|
|
1022
|
+
nodes.map(async (node) => {
|
|
1023
|
+
const context = {
|
|
1024
|
+
ancestorTags: [...node.context.ancestorTags],
|
|
1025
|
+
parentAttributes: { ...node.context.parentAttributes },
|
|
1026
|
+
parentTag: node.context.parentTag,
|
|
1027
|
+
path: [...node.context.path]
|
|
1028
|
+
};
|
|
1029
|
+
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1030
|
+
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1031
|
+
const transformed = await transform(node.decodedText, context);
|
|
1032
|
+
if (typeof transformed !== "string") {
|
|
1033
|
+
throw new TypeError("SSML text node transform must return a string");
|
|
1034
|
+
}
|
|
1035
|
+
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1036
|
+
})
|
|
1037
|
+
);
|
|
1038
|
+
let result = "";
|
|
1039
|
+
let cursor = 0;
|
|
1040
|
+
nodes.forEach((node, nodeIndex) => {
|
|
1041
|
+
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1042
|
+
cursor = node.sourceEnd;
|
|
1043
|
+
});
|
|
1044
|
+
return result + ssml.slice(cursor);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// packages/ssml-core/src/azureValidation.ts
|
|
1048
|
+
var EXPRESS_AS_STYLES = {
|
|
1049
|
+
"en-us-jennyneural": [
|
|
1050
|
+
"assistant",
|
|
1051
|
+
"chat",
|
|
1052
|
+
"customerservice",
|
|
1053
|
+
"newscast",
|
|
1054
|
+
"cheerful",
|
|
1055
|
+
"empathetic",
|
|
1056
|
+
"excited",
|
|
1057
|
+
"friendly",
|
|
1058
|
+
"hopeful",
|
|
1059
|
+
"sad",
|
|
1060
|
+
"shouting",
|
|
1061
|
+
"terrified",
|
|
1062
|
+
"unfriendly",
|
|
1063
|
+
"whispering"
|
|
1064
|
+
],
|
|
1065
|
+
"en-us-guyneural": [
|
|
1066
|
+
"angry",
|
|
1067
|
+
"cheerful",
|
|
1068
|
+
"excited",
|
|
1069
|
+
"friendly",
|
|
1070
|
+
"hopeful",
|
|
1071
|
+
"newscast",
|
|
1072
|
+
"sad",
|
|
1073
|
+
"shouting",
|
|
1074
|
+
"terrified",
|
|
1075
|
+
"unfriendly",
|
|
1076
|
+
"whispering"
|
|
1077
|
+
],
|
|
1078
|
+
"en-us-jennymultilingualneural": [
|
|
1079
|
+
"cheerful",
|
|
1080
|
+
"empathetic",
|
|
1081
|
+
"excited",
|
|
1082
|
+
"friendly",
|
|
1083
|
+
"hopeful",
|
|
1084
|
+
"sad",
|
|
1085
|
+
"shouting",
|
|
1086
|
+
"terrified",
|
|
1087
|
+
"unfriendly",
|
|
1088
|
+
"whispering"
|
|
1089
|
+
],
|
|
1090
|
+
"en-us-andrewneural": ["empathetic", "relieved"],
|
|
1091
|
+
"ja-jp-mayuneural": ["calm", "cheerful", "sad"],
|
|
1092
|
+
"ja-jp-nanamineural": ["chat", "customerservice", "cheerful", "whispering", "sad"],
|
|
1093
|
+
"ja-jp-keitaneural": ["chat"],
|
|
1094
|
+
"ko-kr-sunhineural": ["cheerful", "sad"],
|
|
1095
|
+
"zh-cn-yunxineural": [
|
|
1096
|
+
"narration-relaxed",
|
|
1097
|
+
"embarrassed",
|
|
1098
|
+
"fearful",
|
|
1099
|
+
"sad",
|
|
1100
|
+
"disgruntled",
|
|
1101
|
+
"serious",
|
|
1102
|
+
"angry",
|
|
1103
|
+
"depressed",
|
|
1104
|
+
"chat",
|
|
1105
|
+
"cheerful",
|
|
1106
|
+
"assistant"
|
|
1107
|
+
],
|
|
1108
|
+
"zh-cn-xiaoxiaoneural": [
|
|
1109
|
+
"assistant",
|
|
1110
|
+
"chat",
|
|
1111
|
+
"customerservice",
|
|
1112
|
+
"newscast",
|
|
1113
|
+
"cheerful",
|
|
1114
|
+
"empathetic",
|
|
1115
|
+
"excited",
|
|
1116
|
+
"friendly",
|
|
1117
|
+
"hopeful",
|
|
1118
|
+
"sad",
|
|
1119
|
+
"terrified",
|
|
1120
|
+
"whispering",
|
|
1121
|
+
"poetry-reading",
|
|
1122
|
+
"sports_commentary",
|
|
1123
|
+
"sports_commentary_excited",
|
|
1124
|
+
"story"
|
|
1125
|
+
],
|
|
1126
|
+
"fr-fr-deniseneural": ["cheerful", "sad"],
|
|
1127
|
+
"fr-fr-henrineural": ["cheerful", "sad"],
|
|
1128
|
+
"pt-br-franciscaneural": ["calm"],
|
|
1129
|
+
"it-it-elsaneural": ["cheerful", "sad"],
|
|
1130
|
+
"de-de-katjaneural": ["cheerful", "sad"],
|
|
1131
|
+
"de-de-conradneural": ["cheerful", "sad"],
|
|
1132
|
+
"ru-ru-svetlananeural": ["cheerful", "sad", "angry", "disgruntled", "embarrassed", "fearful"]
|
|
1133
|
+
};
|
|
1134
|
+
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1135
|
+
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1136
|
+
"characters",
|
|
1137
|
+
"spell-out",
|
|
1138
|
+
"cardinal",
|
|
1139
|
+
"ordinal",
|
|
1140
|
+
"number",
|
|
1141
|
+
"date",
|
|
1142
|
+
"time",
|
|
1143
|
+
"telephone",
|
|
1144
|
+
"fraction",
|
|
1145
|
+
"address",
|
|
1146
|
+
"name",
|
|
1147
|
+
"currency"
|
|
1148
|
+
]);
|
|
1149
|
+
var ALLOWED_ROLES = /* @__PURE__ */ new Set([
|
|
1150
|
+
"Girl",
|
|
1151
|
+
"Boy",
|
|
1152
|
+
"YoungAdultFemale",
|
|
1153
|
+
"YoungAdultMale",
|
|
1154
|
+
"OlderAdultFemale",
|
|
1155
|
+
"OlderAdultMale",
|
|
1156
|
+
"SeniorFemale",
|
|
1157
|
+
"SeniorMale"
|
|
1158
|
+
]);
|
|
1159
|
+
var ALLOWED_EMPHASIS_LEVELS = /* @__PURE__ */ new Set(["strong", "moderate", "reduced", "none"]);
|
|
1160
|
+
var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
|
|
1161
|
+
"Leading",
|
|
1162
|
+
"Tailing",
|
|
1163
|
+
"Sentenceboundary",
|
|
1164
|
+
"Comma",
|
|
1165
|
+
"Semicolon",
|
|
1166
|
+
"Enumerationcomma"
|
|
1167
|
+
]);
|
|
1168
|
+
var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
|
|
1169
|
+
function decodeAttribute(value) {
|
|
1170
|
+
return value.replace(
|
|
1171
|
+
/&(?:amp|apos|gt|lt|quot);/gi,
|
|
1172
|
+
(entity) => ({ "&": "&", "'": "'", ">": ">", "<": "<", """: '"' })[entity.toLowerCase()] ?? entity
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
function findTagEnd2(source, start) {
|
|
1176
|
+
let quote = "";
|
|
1177
|
+
for (let index = start; index < source.length; index += 1) {
|
|
1178
|
+
const character = source[index];
|
|
1179
|
+
if (quote) {
|
|
1180
|
+
if (character === quote) quote = "";
|
|
1181
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
1182
|
+
else if (character === ">") return index;
|
|
1183
|
+
}
|
|
1184
|
+
return source.length - 1;
|
|
1185
|
+
}
|
|
1186
|
+
function tokenizeElements(source) {
|
|
1187
|
+
const tokens = [];
|
|
1188
|
+
const openElements = [];
|
|
1189
|
+
let index = 0;
|
|
1190
|
+
while (index < source.length) {
|
|
1191
|
+
const start = source.indexOf("<", index);
|
|
1192
|
+
if (start === -1) break;
|
|
1193
|
+
if (source.startsWith("<!--", start)) {
|
|
1194
|
+
const end2 = source.indexOf("-->", start + 4);
|
|
1195
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (source.startsWith("<![CDATA[", start)) {
|
|
1199
|
+
const end2 = source.indexOf("]]>", start + 9);
|
|
1200
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (source.startsWith("<?", start)) {
|
|
1204
|
+
const end2 = source.indexOf("?>", start + 2);
|
|
1205
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const end = findTagEnd2(source, start + 1);
|
|
1209
|
+
const raw = source.slice(start, end + 1);
|
|
1210
|
+
if (raw.startsWith("</")) {
|
|
1211
|
+
openElements.pop();
|
|
1212
|
+
index = end + 1;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const nameMatch = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(raw);
|
|
1216
|
+
if (!nameMatch?.[1]) {
|
|
1217
|
+
index = end + 1;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1221
|
+
const attributeSource = raw.slice(nameMatch[0].length, raw.length - 1).replace(/\/\s*$/, "");
|
|
1222
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1223
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1224
|
+
attributes.set(match[1].toLowerCase(), decodeAttribute(match[3]));
|
|
1225
|
+
}
|
|
1226
|
+
const selfClosing = /\/\s*>$/.test(raw);
|
|
1227
|
+
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1228
|
+
tokens.push({ attributes, end, name: nameMatch[1], parentVoiceName, selfClosing, start });
|
|
1229
|
+
if (!selfClosing) {
|
|
1230
|
+
openElements.push({
|
|
1231
|
+
name: nameMatch[1],
|
|
1232
|
+
voiceName: nameMatch[1].toLowerCase() === "voice" ? attributes.get("name") : parentVoiceName
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
index = end + 1;
|
|
1236
|
+
}
|
|
1237
|
+
return tokens;
|
|
1238
|
+
}
|
|
1239
|
+
function location(source, offset) {
|
|
1240
|
+
const before = source.slice(0, Math.max(0, offset));
|
|
1241
|
+
const line = before.split("\n").length;
|
|
1242
|
+
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
1243
|
+
}
|
|
1244
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error") {
|
|
1245
|
+
diagnostics.push({ ...location(source, offset), message, severity });
|
|
1246
|
+
}
|
|
1247
|
+
function attr(token, name) {
|
|
1248
|
+
return token.attributes.get(name.toLowerCase());
|
|
1249
|
+
}
|
|
1250
|
+
function normalizeVoiceStyleMap(customVoiceStyleMap) {
|
|
1251
|
+
const map = new Map(
|
|
1252
|
+
Object.entries(EXPRESS_AS_STYLES).map(([voiceName, styles]) => [voiceName.toLowerCase(), styles])
|
|
1253
|
+
);
|
|
1254
|
+
for (const [voiceName, styles] of Object.entries(customVoiceStyleMap ?? {})) {
|
|
1255
|
+
map.set(
|
|
1256
|
+
voiceName.toLowerCase(),
|
|
1257
|
+
styles.map((style) => style.toLowerCase())
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
return map;
|
|
1261
|
+
}
|
|
1262
|
+
function diagnosticSeverity(policy) {
|
|
1263
|
+
if (policy === "ignore") return void 0;
|
|
1264
|
+
return policy === "error" ? "error" : "warning";
|
|
1265
|
+
}
|
|
1266
|
+
function validateElement(token, source, diagnostics, voiceName, options, voiceStyleMap) {
|
|
1267
|
+
const name = token.name.toLowerCase();
|
|
1268
|
+
if (name === "voice" && !attr(token, "name")?.trim())
|
|
1269
|
+
addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
|
|
1270
|
+
if (name === "break") {
|
|
1271
|
+
const time = attr(token, "time");
|
|
1272
|
+
const strength = attr(token, "strength");
|
|
1273
|
+
if (!time && !strength)
|
|
1274
|
+
addDiagnostic(diagnostics, source, token.start, '<break> requires either "time" or "strength".');
|
|
1275
|
+
if (time && strength)
|
|
1276
|
+
addDiagnostic(diagnostics, source, token.start, '<break> must not specify both "time" and "strength".');
|
|
1277
|
+
if (time && !/^\d+(?:\.\d+)?(?:ms|s)$/.test(time.trim()))
|
|
1278
|
+
addDiagnostic(diagnostics, source, token.start, '<break time> must use a numeric value followed by "ms" or "s".');
|
|
1279
|
+
if (strength && !ALLOWED_BREAK_STRENGTHS.has(strength))
|
|
1280
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <break strength> value "${strength}".`);
|
|
1281
|
+
}
|
|
1282
|
+
if (name === "prosody") {
|
|
1283
|
+
const rate = attr(token, "rate");
|
|
1284
|
+
const pitch = attr(token, "pitch");
|
|
1285
|
+
const volume = attr(token, "volume");
|
|
1286
|
+
if (rate && !/^(x-slow|slow|medium|fast|x-fast|[+-]?\d+(?:\.\d+)?%)$/.test(rate.trim()))
|
|
1287
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody rate> value "${rate}".`);
|
|
1288
|
+
if (pitch && !/^(x-low|low|medium|high|x-high|[+-]?\d+(?:\.\d+)?(?:st|Hz|%)?)$/.test(pitch.trim()))
|
|
1289
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody pitch> value "${pitch}".`);
|
|
1290
|
+
if (volume && !/^(silent|x-soft|soft|medium|loud|x-loud|[+-]?\d+(?:\.\d+)?(?:dB|%)?)$/.test(volume.trim()))
|
|
1291
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <prosody volume> value "${volume}".`);
|
|
1292
|
+
}
|
|
1293
|
+
if (name === "mstts:express-as" || name === "express-as" || name === "expressas") {
|
|
1294
|
+
const style = attr(token, "style");
|
|
1295
|
+
if (!style?.trim())
|
|
1296
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:express-as> requires a non-empty "style" attribute.');
|
|
1297
|
+
const degree = attr(token, "styledegree") ?? attr(token, "style-degree");
|
|
1298
|
+
if (degree && (!/^\d+(?:\.\d+)?$/.test(degree) || Number(degree) < 0.01 || Number(degree) > 2))
|
|
1299
|
+
addDiagnostic(
|
|
1300
|
+
diagnostics,
|
|
1301
|
+
source,
|
|
1302
|
+
token.start,
|
|
1303
|
+
"<mstts:express-as styledegree> must be a number between 0.01 and 2."
|
|
1304
|
+
);
|
|
1305
|
+
const role = attr(token, "role");
|
|
1306
|
+
if (role && !ALLOWED_ROLES.has(role))
|
|
1307
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <mstts:express-as role> value "${role}".`);
|
|
1308
|
+
const supportedStyles = voiceName ? voiceStyleMap.get(voiceName.toLowerCase()) : void 0;
|
|
1309
|
+
const severity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
1310
|
+
if (style && supportedStyles && !supportedStyles.includes(style.toLowerCase()) && severity)
|
|
1311
|
+
addDiagnostic(
|
|
1312
|
+
diagnostics,
|
|
1313
|
+
source,
|
|
1314
|
+
token.start,
|
|
1315
|
+
`Unknown style "${style}" is not supported by voice "${voiceName}" according to the configured voice style map.`,
|
|
1316
|
+
severity
|
|
1317
|
+
);
|
|
1318
|
+
if (style && voiceName && !supportedStyles && severity)
|
|
1319
|
+
addDiagnostic(
|
|
1320
|
+
diagnostics,
|
|
1321
|
+
source,
|
|
1322
|
+
token.start,
|
|
1323
|
+
`Unknown style "${style}" cannot be verified because voice "${voiceName}" is not registered in the voice style map.`,
|
|
1324
|
+
severity
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (name === "say-as" || name === "sayas") {
|
|
1328
|
+
const interpretAs = attr(token, "interpret-as");
|
|
1329
|
+
if (!interpretAs || !ALLOWED_SAY_AS.has(interpretAs))
|
|
1330
|
+
addDiagnostic(diagnostics, source, token.start, `<say-as> requires a supported "interpret-as" value.`);
|
|
1331
|
+
}
|
|
1332
|
+
if (name === "phoneme" && (!attr(token, "alphabet") || !attr(token, "ph")))
|
|
1333
|
+
addDiagnostic(diagnostics, source, token.start, '<phoneme> requires both "alphabet" and "ph" attributes.');
|
|
1334
|
+
if (name === "emphasis" && attr(token, "level") && !ALLOWED_EMPHASIS_LEVELS.has(attr(token, "level") ?? ""))
|
|
1335
|
+
addDiagnostic(diagnostics, source, token.start, `Unsupported <emphasis level> value "${attr(token, "level")}".`);
|
|
1336
|
+
if (name === "sub" && !attr(token, "alias")?.trim())
|
|
1337
|
+
addDiagnostic(diagnostics, source, token.start, '<sub> requires a non-empty "alias" attribute.');
|
|
1338
|
+
if (name === "lang" && !attr(token, "xml:lang")?.trim() && !attr(token, "lang")?.trim())
|
|
1339
|
+
addDiagnostic(diagnostics, source, token.start, '<lang> requires an "xml:lang" attribute.');
|
|
1340
|
+
if (name === "mark" && !attr(token, "name")?.trim())
|
|
1341
|
+
addDiagnostic(diagnostics, source, token.start, '<mark> requires a non-empty "name" attribute.');
|
|
1342
|
+
if (name === "bookmark" && !attr(token, "mark")?.trim())
|
|
1343
|
+
addDiagnostic(diagnostics, source, token.start, '<bookmark> requires a non-empty "mark" attribute.');
|
|
1344
|
+
if (name === "lexicon") {
|
|
1345
|
+
const uri = attr(token, "uri");
|
|
1346
|
+
if (!uri) addDiagnostic(diagnostics, source, token.start, '<lexicon> requires a "uri" attribute.');
|
|
1347
|
+
else {
|
|
1348
|
+
try {
|
|
1349
|
+
const parsed = new URL(uri);
|
|
1350
|
+
if (parsed.protocol !== "https:")
|
|
1351
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must use HTTPS.");
|
|
1352
|
+
} catch {
|
|
1353
|
+
addDiagnostic(diagnostics, source, token.start, "<lexicon uri> must be an absolute HTTPS URL.");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (name === "mstts:silence") {
|
|
1358
|
+
const type = attr(token, "type");
|
|
1359
|
+
const value = attr(token, "value");
|
|
1360
|
+
if (!type || !ALLOWED_SILENCE_TYPES.has(type))
|
|
1361
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a supported "type" attribute.');
|
|
1362
|
+
if (!value || !/^\d+(?:\.\d+)?(?:ms|s)$/.test(value.trim()))
|
|
1363
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:silence> requires a time-valued "value" attribute.');
|
|
1364
|
+
}
|
|
1365
|
+
if (name === "mstts:viseme") {
|
|
1366
|
+
const type = attr(token, "type");
|
|
1367
|
+
if (!type || !ALLOWED_VISEME_TYPES.has(type))
|
|
1368
|
+
addDiagnostic(diagnostics, source, token.start, '<mstts:viseme> requires a supported "type" attribute.');
|
|
1369
|
+
}
|
|
1370
|
+
if (name === "audio") {
|
|
1371
|
+
const src = attr(token, "src");
|
|
1372
|
+
if (!src) addDiagnostic(diagnostics, source, token.start, '<audio> requires a "src" attribute.');
|
|
1373
|
+
else {
|
|
1374
|
+
let parsed;
|
|
1375
|
+
try {
|
|
1376
|
+
parsed = new URL(src);
|
|
1377
|
+
} catch {
|
|
1378
|
+
addDiagnostic(diagnostics, source, token.start, "<audio src> must be an absolute HTTP(S) URL.");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1382
|
+
addDiagnostic(diagnostics, source, token.start, "<audio src> must use HTTPS.");
|
|
1383
|
+
if (options.allowedAudioOrigins && !options.allowedAudioOrigins.includes(parsed.origin))
|
|
1384
|
+
addDiagnostic(diagnostics, source, token.start, `<audio src> origin "${parsed.origin}" is not allowed.`);
|
|
1385
|
+
else if (!options.allowExternalAudio)
|
|
1386
|
+
addDiagnostic(
|
|
1387
|
+
diagnostics,
|
|
1388
|
+
source,
|
|
1389
|
+
token.start,
|
|
1390
|
+
`<audio src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`,
|
|
1391
|
+
"error"
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
1397
|
+
const diagnostics = [];
|
|
1398
|
+
if (typeof ssml !== "string") {
|
|
1399
|
+
return [{ line: 1, column: 1, message: "SSML input must be a string", severity: "error" }];
|
|
1400
|
+
}
|
|
1401
|
+
const maxLength = options.maxLength ?? 1e4;
|
|
1402
|
+
if (ssml.length > maxLength)
|
|
1403
|
+
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
1404
|
+
try {
|
|
1405
|
+
parseSsml(ssml);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
const message = error instanceof Error ? error.message.replace(/ at position \d+$/, "") : String(error);
|
|
1408
|
+
const match = / at position (\d+)$/.exec(error instanceof Error ? error.message : "");
|
|
1409
|
+
addDiagnostic(diagnostics, ssml, match ? Number(match[1]) : 0, message);
|
|
1410
|
+
return diagnostics;
|
|
1411
|
+
}
|
|
1412
|
+
const tokens = tokenizeElements(ssml);
|
|
1413
|
+
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
1414
|
+
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
1415
|
+
if (!speak || voices.length === 0)
|
|
1416
|
+
addDiagnostic(
|
|
1417
|
+
diagnostics,
|
|
1418
|
+
ssml,
|
|
1419
|
+
speak?.start ?? 0,
|
|
1420
|
+
"Azure SSML requires at least one <voice> element under <speak>."
|
|
1421
|
+
);
|
|
1422
|
+
const voiceName = voices[0] ? attr(voices[0], "name") : void 0;
|
|
1423
|
+
const voiceStyleMap = normalizeVoiceStyleMap(options.customVoiceStyleMap);
|
|
1424
|
+
const policySeverity = diagnosticSeverity(options.unknownVoicePolicy ?? "warn");
|
|
1425
|
+
const voicesToValidate = options.validateNestedVoices === false ? voices.slice(0, 1) : voices;
|
|
1426
|
+
for (const token of voicesToValidate) {
|
|
1427
|
+
const name = attr(token, "name")?.trim();
|
|
1428
|
+
if (name && !voiceStyleMap.has(name.toLowerCase()) && policySeverity)
|
|
1429
|
+
addDiagnostic(
|
|
1430
|
+
diagnostics,
|
|
1431
|
+
ssml,
|
|
1432
|
+
token.start,
|
|
1433
|
+
`Unknown voice "${name}" is not registered in the voice style map.`,
|
|
1434
|
+
policySeverity
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
for (const token of tokens) {
|
|
1438
|
+
const tokenVoiceName = options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1439
|
+
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceStyleMap);
|
|
1440
|
+
}
|
|
1441
|
+
return diagnostics;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
895
1444
|
// packages/azure-tts-client/src/errors.ts
|
|
896
1445
|
var AzureTtsError = class extends Error {
|
|
897
1446
|
constructor(status, statusText, responseBody, requestId) {
|
|
@@ -976,7 +1525,8 @@ function resolveOutputFormat(outputFormat) {
|
|
|
976
1525
|
|
|
977
1526
|
// packages/azure-tts-client/src/speechConfig.ts
|
|
978
1527
|
function resolveEndpoint(config) {
|
|
979
|
-
|
|
1528
|
+
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
1529
|
+
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
980
1530
|
}
|
|
981
1531
|
function createSpeechConfig(config) {
|
|
982
1532
|
const { outputFormat = DEFAULT_OUTPUT_FORMAT, subscriptionKey } = config;
|
|
@@ -998,30 +1548,56 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
998
1548
|
}
|
|
999
1549
|
}
|
|
1000
1550
|
async function synthesizeSpeech(ssml, config) {
|
|
1551
|
+
if (config.signal?.aborted) {
|
|
1552
|
+
throw createSpeechSdkError("Speech synthesis was cancelled.");
|
|
1553
|
+
}
|
|
1001
1554
|
const speechConfig = createSpeechConfig(config);
|
|
1002
1555
|
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
1003
1556
|
return await new Promise((resolve, reject) => {
|
|
1004
1557
|
let resourcesClosed = false;
|
|
1558
|
+
let settled = false;
|
|
1559
|
+
let timeout;
|
|
1560
|
+
let abortHandler;
|
|
1561
|
+
const cleanup = () => {
|
|
1562
|
+
if (timeout) clearTimeout(timeout);
|
|
1563
|
+
if (abortHandler) config.signal?.removeEventListener("abort", abortHandler);
|
|
1564
|
+
};
|
|
1005
1565
|
const closeResources = () => {
|
|
1006
1566
|
if (resourcesClosed) return;
|
|
1007
1567
|
resourcesClosed = true;
|
|
1008
1568
|
closeSpeechResources(speechConfig, synthesizer);
|
|
1009
1569
|
};
|
|
1010
1570
|
const rejectWithError = (error) => {
|
|
1571
|
+
if (settled) return;
|
|
1572
|
+
settled = true;
|
|
1573
|
+
cleanup();
|
|
1011
1574
|
closeResources();
|
|
1012
1575
|
reject(createSpeechSdkError(error));
|
|
1013
1576
|
};
|
|
1014
1577
|
const cb = (result) => {
|
|
1578
|
+
if (settled) return;
|
|
1015
1579
|
const { reason, errorDetails } = result;
|
|
1016
1580
|
if (reason !== SpeechSDK2.ResultReason.SynthesizingAudioCompleted) {
|
|
1017
1581
|
const err = errorDetails || `Speech synthesis failed with reason ${reason}.`;
|
|
1018
1582
|
rejectWithError(err);
|
|
1019
1583
|
return;
|
|
1020
1584
|
}
|
|
1585
|
+
settled = true;
|
|
1586
|
+
cleanup();
|
|
1021
1587
|
closeResources();
|
|
1022
1588
|
resolve(result.audioData);
|
|
1023
1589
|
};
|
|
1024
1590
|
try {
|
|
1591
|
+
if (config.signal) {
|
|
1592
|
+
abortHandler = () => rejectWithError("Speech synthesis was cancelled.");
|
|
1593
|
+
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
1594
|
+
}
|
|
1595
|
+
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
1596
|
+
timeout = setTimeout(
|
|
1597
|
+
() => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
|
|
1598
|
+
config.timeoutMs
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1025
1601
|
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
1026
1602
|
} catch (error) {
|
|
1027
1603
|
rejectWithError(error);
|
|
@@ -1038,10 +1614,10 @@ var AzureTtsClient = class {
|
|
|
1038
1614
|
__privateSet(this, _options, options);
|
|
1039
1615
|
}
|
|
1040
1616
|
async synthesize(ssml) {
|
|
1041
|
-
const { region, subscriptionKey, outputFormat } = __privateGet(this, _options);
|
|
1042
|
-
const endpoint = __privateGet(this, _options).endpoint
|
|
1043
|
-
|
|
1044
|
-
const config = { endpoint, region, subscriptionKey, outputFormat };
|
|
1617
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1618
|
+
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1619
|
+
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1620
|
+
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
1045
1621
|
return synthesizeSpeech(ssml, config);
|
|
1046
1622
|
}
|
|
1047
1623
|
};
|
|
@@ -1053,8 +1629,11 @@ _options = new WeakMap();
|
|
|
1053
1629
|
AzureTtsSdkError,
|
|
1054
1630
|
buildPartialSsml,
|
|
1055
1631
|
buildSsml,
|
|
1632
|
+
extractSsmlText,
|
|
1633
|
+
mapSsmlTextNodes,
|
|
1056
1634
|
parseSsml,
|
|
1057
1635
|
synthesizeSpeech,
|
|
1636
|
+
validateAzureSsml,
|
|
1058
1637
|
validateSsml
|
|
1059
1638
|
});
|
|
1060
1639
|
//# sourceMappingURL=index.js.map
|