ssml-builder-js 2.11.0 → 2.13.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 +61 -3
- package/dist/{chunk-SNRI43QJ.mjs → chunk-25LOR4AJ.mjs} +248 -4
- package/dist/chunk-25LOR4AJ.mjs.map +1 -0
- package/dist/chunk-CZ2F3TET.mjs +1745 -0
- package/dist/chunk-CZ2F3TET.mjs.map +1 -0
- package/dist/{chunk-FWWMWI2C.mjs → chunk-LCTE26LS.mjs} +248 -1701
- package/dist/chunk-LCTE26LS.mjs.map +1 -0
- package/dist/core.d.mts +54 -2
- package/dist/core.d.ts +54 -2
- package/dist/core.js +248 -3
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +141 -3
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +6 -4
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d-Xbr6ZWnK.d.mts +214 -0
- package/dist/index.d-Xbr6ZWnK.d.ts +214 -0
- package/dist/index.d.mts +123 -2
- package/dist/index.d.ts +123 -2
- package/dist/index.js +1882 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +61 -163
- package/dist/react.d.ts +61 -163
- package/dist/react.js +557 -16
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +276 -17
- package/dist/react.mjs.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-FWWMWI2C.mjs.map +0 -1
- package/dist/chunk-SNRI43QJ.mjs.map +0 -1
package/dist/core.d.ts
CHANGED
|
@@ -188,6 +188,36 @@ interface BuildPartialSsmlOptions extends SsmlPartialContext {
|
|
|
188
188
|
declare function buildPartialSsml(text: string, context?: SsmlPartialContext): string;
|
|
189
189
|
declare function buildPartialSsml(options: BuildPartialSsmlOptions): string;
|
|
190
190
|
|
|
191
|
+
interface SsmlTextRange {
|
|
192
|
+
start: number;
|
|
193
|
+
end: number;
|
|
194
|
+
}
|
|
195
|
+
interface SsmlChunkContext {
|
|
196
|
+
voice?: string;
|
|
197
|
+
lang?: string;
|
|
198
|
+
prosody?: Record<string, string>;
|
|
199
|
+
}
|
|
200
|
+
interface SsmlChunk {
|
|
201
|
+
chunkIndex: number;
|
|
202
|
+
ssml: string;
|
|
203
|
+
originalTextRange: SsmlTextRange;
|
|
204
|
+
inheritedContext: SsmlChunkContext;
|
|
205
|
+
containedMarks: string[];
|
|
206
|
+
hasBackgroundAudio: boolean;
|
|
207
|
+
}
|
|
208
|
+
interface SplitSsmlOptions {
|
|
209
|
+
maxLength?: number;
|
|
210
|
+
/** Keeps `<mstts:backgroundaudio>` in every chunk instead of the first chunk only. */
|
|
211
|
+
replicateBackgroundAudio?: boolean;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Splits SSML into independently synthesizable documents without breaking XML
|
|
215
|
+
* elements. Parent elements such as `<voice>` and `<prosody>` are copied into
|
|
216
|
+
* every block. Paragraph and sentence elements therefore remain the natural
|
|
217
|
+
* split points, while oversized text is split at word/character boundaries.
|
|
218
|
+
*/
|
|
219
|
+
declare function splitSsmlDocument(ssml: string, maxLength?: number | SplitSsmlOptions, options?: SplitSsmlOptions): SsmlChunk[];
|
|
220
|
+
|
|
191
221
|
declare function parseSsml(xmlString: string): SsmlDocument;
|
|
192
222
|
|
|
193
223
|
interface SsmlValidationError {
|
|
@@ -284,8 +314,14 @@ interface AzureValidationOptions {
|
|
|
284
314
|
allowHttpAudio?: boolean;
|
|
285
315
|
customVoiceStyleMap?: Record<string, readonly string[]>;
|
|
286
316
|
customVoiceDefinitions?: readonly AzureVoiceDefinition[];
|
|
317
|
+
/** Host-side validation hook for URL-bearing SSML attributes. */
|
|
318
|
+
urlValidator?: AzureUrlValidator;
|
|
319
|
+
/** Alias for urlValidator retained for applications that use the longer name. */
|
|
320
|
+
customUrlValidator?: AzureUrlValidator;
|
|
287
321
|
languageAliases?: Record<string, string | readonly string[]>;
|
|
288
322
|
maxLength?: number;
|
|
323
|
+
/** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
|
|
324
|
+
maxXmlDepth?: number;
|
|
289
325
|
/** Optional model identifier used to validate a voice's model compatibility. */
|
|
290
326
|
model?: string;
|
|
291
327
|
normalizeLanguage?: (lang: string) => string;
|
|
@@ -301,6 +337,14 @@ interface AzureValidationOptions {
|
|
|
301
337
|
voiceCatalog?: readonly AzureVoiceDefinition[];
|
|
302
338
|
voiceDefinitions?: readonly AzureVoiceDefinition[];
|
|
303
339
|
}
|
|
340
|
+
type AzureUrlValidationResult = boolean | {
|
|
341
|
+
valid: boolean;
|
|
342
|
+
reason?: string;
|
|
343
|
+
};
|
|
344
|
+
type AzureUrlValidator = (url: string, context: {
|
|
345
|
+
tag: string;
|
|
346
|
+
attribute: string;
|
|
347
|
+
}) => AzureUrlValidationResult | Promise<AzureUrlValidationResult>;
|
|
304
348
|
type AzureLanguageNormalizationOptions = Pick<AzureValidationOptions, "languageAliases" | "normalizeLanguage">;
|
|
305
349
|
/** @deprecated Use AzureValidationOptions instead. */
|
|
306
350
|
type AzureSsmlValidationOptions = AzureValidationOptions;
|
|
@@ -310,7 +354,15 @@ declare function isValidAzureAudioDuration(value: string): boolean;
|
|
|
310
354
|
declare function normalizeAzureLanguage(language: string, options?: AzureLanguageNormalizationOptions): string;
|
|
311
355
|
/** Compares two Azure language tags after BCP 47 and alias normalization. */
|
|
312
356
|
declare function areAzureLanguagesEquivalent(first: string, second: string, options?: AzureLanguageNormalizationOptions): boolean;
|
|
313
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Validates Azure SSML synchronously unless a URL validator is supplied. URL
|
|
359
|
+
* validation is asynchronous-capable so hosts can perform DNS/private-network checks.
|
|
360
|
+
*/
|
|
361
|
+
declare function validateAzureSsml(ssml: string, options?: Omit<AzureValidationOptions, "urlValidator" | "customUrlValidator">): SsmlDiagnostic[];
|
|
362
|
+
declare function validateAzureSsml(ssml: string, options: AzureValidationOptions & {
|
|
363
|
+
urlValidator?: AzureUrlValidator;
|
|
364
|
+
customUrlValidator?: AzureUrlValidator;
|
|
365
|
+
}): SsmlDiagnostic[] | Promise<SsmlDiagnostic[]>;
|
|
314
366
|
|
|
315
367
|
interface AzureVoiceCatalogMetadata {
|
|
316
368
|
apiVersion: string;
|
|
@@ -324,4 +376,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
|
|
|
324
376
|
/** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
|
|
325
377
|
declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
|
|
326
378
|
|
|
327
|
-
export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
|
|
379
|
+
export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureUrlValidationResult, type AzureUrlValidator, type AzureValidationOptions, type AzureVoiceCatalogMetadata, type AzureVoiceDefinition, type AzureVoiceMetadata, type BookmarkElement, type BreakElement, type BuildPartialSsmlOptions, type CustomElement, type EmphasisElement, type ExpressAsElement, type ExtractSsmlTranslatableTextOptions, type FromPlainTextToSsmlOptions, type LangElement, type LexiconElement, type MapSsmlTextNodesOptions, type MarkElement, type MsttsAudioDurationElement, type MsttsEmbeddingElement, type MsttsSilenceElement, type MsttsTtsEmbeddingElement, type MsttsVisemeElement, type MsttsVoiceConversionElement, type NamedElement, type ParagraphElement, type PhonemeElement, type ProsodyElement, type SayAsElement, type SentenceElement, type SplitSsmlOptions, type SsmlAttributeValue, type SsmlAttributes, type SsmlBackgroundAudioNode, type SsmlBreakElement, type SsmlChunk, type SsmlChunkContext, type SsmlDiagnostic, type SsmlDiagnosticSeverity, type SsmlDiagnosticSource, type SsmlDialogNode, type SsmlDocument, type SsmlElement, type SsmlElementBase, type SsmlEmbeddingNode, type SsmlExpressAsElement, type SsmlNode, type SsmlPartialContext, type SsmlPartialProsody, type SsmlPartialVoice, type SsmlPhonemeElement, type SsmlProsodyElement, type SsmlSayAsElement, type SsmlStructureIntegrityResult, type SsmlStructureMismatch, type SsmlText, type SsmlTextNodeContext, type SsmlTextRange, type SsmlTtsEmbeddingNode, type SsmlTurnNode, type SsmlValidationError, type SsmlVoiceConversionNode, type SsmlVoiceElement, type SubElement, type VoiceElement, type WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
|
package/dist/core.js
CHANGED
|
@@ -39,6 +39,7 @@ __export(core_exports, {
|
|
|
39
39
|
mapSsmlTextNodes: () => mapSsmlTextNodes,
|
|
40
40
|
normalizeAzureLanguage: () => normalizeAzureLanguage,
|
|
41
41
|
parseSsml: () => parseSsml,
|
|
42
|
+
splitSsmlDocument: () => splitSsmlDocument,
|
|
42
43
|
validateAzureSsml: () => validateAzureSsml,
|
|
43
44
|
validateSsml: () => validateSsml,
|
|
44
45
|
validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
|
|
@@ -972,6 +973,183 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
972
973
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
973
974
|
}
|
|
974
975
|
|
|
976
|
+
// packages/ssml-core/src/split.ts
|
|
977
|
+
var DEFAULT_MAX_LENGTH = 1e4;
|
|
978
|
+
function cloneElement(element, children) {
|
|
979
|
+
return { ...element, children };
|
|
980
|
+
}
|
|
981
|
+
function documentWithChildren(document, children) {
|
|
982
|
+
return buildSsml({ ...document, children });
|
|
983
|
+
}
|
|
984
|
+
function wrapWithContext(node, context) {
|
|
985
|
+
return context.reduceRight((current, parent) => cloneElement(parent, [current]), node);
|
|
986
|
+
}
|
|
987
|
+
function documentWithNode(document, node, context) {
|
|
988
|
+
return documentWithChildren(document, [wrapWithContext(node, context)]);
|
|
989
|
+
}
|
|
990
|
+
function splitTextNode(text, document, context, maxLength) {
|
|
991
|
+
if (!text) return [text];
|
|
992
|
+
const parts = [];
|
|
993
|
+
let start = 0;
|
|
994
|
+
while (start < text.length) {
|
|
995
|
+
let end = start + 1;
|
|
996
|
+
let bestEnd = end;
|
|
997
|
+
while (end <= text.length) {
|
|
998
|
+
if (documentWithNode(document, text.slice(start, end), context).length > maxLength) break;
|
|
999
|
+
bestEnd = end;
|
|
1000
|
+
end += 1;
|
|
1001
|
+
}
|
|
1002
|
+
if (bestEnd === start) {
|
|
1003
|
+
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1004
|
+
}
|
|
1005
|
+
const segment = text.slice(start, bestEnd);
|
|
1006
|
+
const boundary = Math.max(segment.lastIndexOf(" "), segment.lastIndexOf("\n"), segment.lastIndexOf(" "));
|
|
1007
|
+
const splitEnd = boundary > 0 ? start + boundary + 1 : bestEnd;
|
|
1008
|
+
parts.push(text.slice(start, splitEnd));
|
|
1009
|
+
start = splitEnd;
|
|
1010
|
+
}
|
|
1011
|
+
return parts;
|
|
1012
|
+
}
|
|
1013
|
+
function splitNode(document, node, maxLength, context = []) {
|
|
1014
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1015
|
+
const value = typeof node === "string" ? node : node.value;
|
|
1016
|
+
if (documentWithNode(document, node, context).length <= maxLength) return [node];
|
|
1017
|
+
return splitTextNode(value, document, context, maxLength);
|
|
1018
|
+
}
|
|
1019
|
+
if (documentWithNode(document, node, context).length <= maxLength) return [node];
|
|
1020
|
+
const children = node.children ?? [];
|
|
1021
|
+
if (children.length === 0) {
|
|
1022
|
+
throw new RangeError(`maxLength is too small to contain <${node.type}>`);
|
|
1023
|
+
}
|
|
1024
|
+
const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength, [...context, node]));
|
|
1025
|
+
const parts = [];
|
|
1026
|
+
let group = [];
|
|
1027
|
+
const flush = () => {
|
|
1028
|
+
if (group.length > 0) {
|
|
1029
|
+
parts.push(cloneElement(node, group));
|
|
1030
|
+
group = [];
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
for (const child of splitChildren) {
|
|
1034
|
+
const candidate = cloneElement(node, [...group, child]);
|
|
1035
|
+
if (documentWithNode(document, candidate, context).length <= maxLength) {
|
|
1036
|
+
group.push(child);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
flush();
|
|
1040
|
+
if (documentWithNode(document, cloneElement(node, [child]), context).length > maxLength) {
|
|
1041
|
+
throw new RangeError(`maxLength is too small to contain <${node.type}>`);
|
|
1042
|
+
}
|
|
1043
|
+
group.push(child);
|
|
1044
|
+
}
|
|
1045
|
+
flush();
|
|
1046
|
+
return parts;
|
|
1047
|
+
}
|
|
1048
|
+
function textFromNode(node) {
|
|
1049
|
+
if (typeof node === "string") return node;
|
|
1050
|
+
if (node.type === "text") return node.value;
|
|
1051
|
+
return (node.children ?? []).map(textFromNode).join("");
|
|
1052
|
+
}
|
|
1053
|
+
function collectMarks(node, marks) {
|
|
1054
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1055
|
+
if (node.type === "mark" && node.name) marks.push(node.name);
|
|
1056
|
+
if (node.type === "bookmark" && node.mark) marks.push(node.mark);
|
|
1057
|
+
for (const child of node.children ?? []) collectMarks(child, marks);
|
|
1058
|
+
}
|
|
1059
|
+
function collectInheritedContext(nodes) {
|
|
1060
|
+
const context = {};
|
|
1061
|
+
const visit = (node) => {
|
|
1062
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1063
|
+
if (context.voice === void 0 && node.type === "voice" && node.name) context.voice = node.name;
|
|
1064
|
+
if (context.lang === void 0 && node.type === "lang" && node.lang) context.lang = node.lang;
|
|
1065
|
+
if (context.prosody === void 0 && node.type === "prosody") {
|
|
1066
|
+
const prosody = {};
|
|
1067
|
+
for (const [key, value] of Object.entries(node.attributes ?? {})) prosody[key] = String(value);
|
|
1068
|
+
for (const key of ["rate", "pitch", "volume", "contour", "range"]) {
|
|
1069
|
+
const value = node[key];
|
|
1070
|
+
if (value !== void 0) prosody[key] = String(value);
|
|
1071
|
+
}
|
|
1072
|
+
if (Object.keys(prosody).length > 0) context.prosody = prosody;
|
|
1073
|
+
}
|
|
1074
|
+
for (const child of node.children ?? []) visit(child);
|
|
1075
|
+
};
|
|
1076
|
+
nodes.forEach(visit);
|
|
1077
|
+
return context;
|
|
1078
|
+
}
|
|
1079
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1080
|
+
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1081
|
+
const text = nodes.map(textFromNode).join("");
|
|
1082
|
+
const marks = [];
|
|
1083
|
+
for (const node of nodes) collectMarks(node, marks);
|
|
1084
|
+
const inheritedContext = collectInheritedContext(nodes);
|
|
1085
|
+
if (inheritedContext.lang === void 0 && document.lang) inheritedContext.lang = document.lang;
|
|
1086
|
+
return {
|
|
1087
|
+
chunkIndex,
|
|
1088
|
+
ssml: documentWithChildren(document, chunkNodes),
|
|
1089
|
+
originalTextRange: { start: textStart, end: textStart + text.length },
|
|
1090
|
+
inheritedContext,
|
|
1091
|
+
containedMarks: marks,
|
|
1092
|
+
hasBackgroundAudio: chunkNodes.some(
|
|
1093
|
+
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1094
|
+
)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
1098
|
+
const resolvedMaxLength = typeof maxLength === "number" ? maxLength : maxLength.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
1099
|
+
const resolvedOptions = typeof maxLength === "number" ? options : maxLength;
|
|
1100
|
+
if (!Number.isInteger(resolvedMaxLength) || resolvedMaxLength <= 0) {
|
|
1101
|
+
throw new RangeError("maxLength must be a positive integer");
|
|
1102
|
+
}
|
|
1103
|
+
const document = parseSsml(ssml);
|
|
1104
|
+
const backgroundAudio = (document.children ?? []).find(
|
|
1105
|
+
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1106
|
+
);
|
|
1107
|
+
if (ssml.length <= resolvedMaxLength) {
|
|
1108
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1109
|
+
}
|
|
1110
|
+
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1111
|
+
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
1112
|
+
const backgroundDocumentLength = backgroundAudio ? documentWithChildren(document, [backgroundAudio]).length : plainDocumentLength;
|
|
1113
|
+
const backgroundOverhead = Math.max(0, backgroundDocumentLength - plainDocumentLength);
|
|
1114
|
+
const contentMaxLength = Math.max(1, resolvedMaxLength - backgroundOverhead);
|
|
1115
|
+
const splitChildren = contentChildren.flatMap((child) => splitNode(document, child, contentMaxLength));
|
|
1116
|
+
const chunks = [];
|
|
1117
|
+
let group = [];
|
|
1118
|
+
for (const child of splitChildren) {
|
|
1119
|
+
const candidate = [...group, child];
|
|
1120
|
+
if (documentWithChildren(document, candidate).length <= contentMaxLength) {
|
|
1121
|
+
group = candidate;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (group.length > 0) chunks.push(group);
|
|
1125
|
+
group = [child];
|
|
1126
|
+
if (documentWithChildren(document, group).length > contentMaxLength) {
|
|
1127
|
+
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (group.length > 0) chunks.push(group);
|
|
1131
|
+
if (chunks.length === 0) {
|
|
1132
|
+
const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
|
|
1133
|
+
if (result.ssml.length > resolvedMaxLength) {
|
|
1134
|
+
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1135
|
+
}
|
|
1136
|
+
return [result];
|
|
1137
|
+
}
|
|
1138
|
+
let textStart = 0;
|
|
1139
|
+
return chunks.map((chunk, chunkIndex) => {
|
|
1140
|
+
const result = createChunk(
|
|
1141
|
+
document,
|
|
1142
|
+
chunk,
|
|
1143
|
+
chunkIndex,
|
|
1144
|
+
textStart,
|
|
1145
|
+
backgroundAudio,
|
|
1146
|
+
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1147
|
+
);
|
|
1148
|
+
textStart = result.originalTextRange.end;
|
|
1149
|
+
return result;
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
975
1153
|
// packages/ssml-core/src/validation.ts
|
|
976
1154
|
var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
|
|
977
1155
|
function validateSsml(xmlString) {
|
|
@@ -1666,6 +1844,7 @@ function tokenizeElements(source) {
|
|
|
1666
1844
|
attributes,
|
|
1667
1845
|
childElementIndex,
|
|
1668
1846
|
end,
|
|
1847
|
+
depth: openElements.length + 1,
|
|
1669
1848
|
name: tokenName,
|
|
1670
1849
|
parentName: parent?.name,
|
|
1671
1850
|
parentVoiceName,
|
|
@@ -1856,13 +2035,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
1856
2035
|
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
|
|
1857
2036
|
return;
|
|
1858
2037
|
}
|
|
2038
|
+
if (parsed.username || parsed.password)
|
|
2039
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
|
|
1859
2040
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1860
2041
|
addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
|
|
1861
2042
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1862
2043
|
try {
|
|
1863
|
-
|
|
2044
|
+
const configured = new URL(allowedOrigin);
|
|
2045
|
+
if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
|
|
2046
|
+
return false;
|
|
2047
|
+
return configured.origin === parsed.origin;
|
|
1864
2048
|
} catch {
|
|
1865
|
-
return
|
|
2049
|
+
return false;
|
|
1866
2050
|
}
|
|
1867
2051
|
}) ?? false;
|
|
1868
2052
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
@@ -2054,7 +2238,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
2054
2238
|
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
2055
2239
|
}
|
|
2056
2240
|
}
|
|
2057
|
-
function
|
|
2241
|
+
function validateAzureSsmlStatic(ssml, options = {}) {
|
|
2058
2242
|
const diagnostics = [];
|
|
2059
2243
|
if (typeof ssml !== "string") {
|
|
2060
2244
|
return [
|
|
@@ -2070,6 +2254,9 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2070
2254
|
const maxLength = options.maxLength ?? 1e4;
|
|
2071
2255
|
if (ssml.length > maxLength)
|
|
2072
2256
|
addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
|
|
2257
|
+
if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
|
|
2258
|
+
addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
|
|
2259
|
+
}
|
|
2073
2260
|
try {
|
|
2074
2261
|
parseSsml(ssml);
|
|
2075
2262
|
} catch (error) {
|
|
@@ -2079,6 +2266,18 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2079
2266
|
return diagnostics;
|
|
2080
2267
|
}
|
|
2081
2268
|
const tokens = tokenizeElements(ssml);
|
|
2269
|
+
if (options.maxXmlDepth !== void 0) {
|
|
2270
|
+
for (const token of tokens) {
|
|
2271
|
+
if (token.depth > options.maxXmlDepth) {
|
|
2272
|
+
addDiagnostic(
|
|
2273
|
+
diagnostics,
|
|
2274
|
+
ssml,
|
|
2275
|
+
token.start,
|
|
2276
|
+
`XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2082
2281
|
const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
|
|
2083
2282
|
const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
|
|
2084
2283
|
const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
|
|
@@ -2163,6 +2362,51 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2163
2362
|
}
|
|
2164
2363
|
return diagnostics;
|
|
2165
2364
|
}
|
|
2365
|
+
function urlAttributes(token) {
|
|
2366
|
+
const tag = canonicalTagName(token.name);
|
|
2367
|
+
const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
|
|
2368
|
+
return attributes.flatMap((attribute) => {
|
|
2369
|
+
const value = attr(token, attribute);
|
|
2370
|
+
return value === void 0 ? [] : [{ attribute, value }];
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
2374
|
+
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2375
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2376
|
+
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2377
|
+
let tokens;
|
|
2378
|
+
try {
|
|
2379
|
+
tokens = tokenizeElements(ssml);
|
|
2380
|
+
} catch {
|
|
2381
|
+
return diagnostics;
|
|
2382
|
+
}
|
|
2383
|
+
const checks = tokens.flatMap(
|
|
2384
|
+
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2385
|
+
try {
|
|
2386
|
+
const result = await validator(value, { tag: token.name, attribute });
|
|
2387
|
+
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2388
|
+
if (!valid) {
|
|
2389
|
+
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
2390
|
+
addDiagnostic(
|
|
2391
|
+
diagnostics,
|
|
2392
|
+
ssml,
|
|
2393
|
+
token.start,
|
|
2394
|
+
`<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2399
|
+
addDiagnostic(
|
|
2400
|
+
diagnostics,
|
|
2401
|
+
ssml,
|
|
2402
|
+
token.start,
|
|
2403
|
+
`<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
})
|
|
2407
|
+
);
|
|
2408
|
+
return Promise.all(checks).then(() => diagnostics);
|
|
2409
|
+
}
|
|
2166
2410
|
|
|
2167
2411
|
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2168
2412
|
var AZURE_VOICE_CATALOG_METADATA = {
|
|
@@ -2194,6 +2438,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2194
2438
|
mapSsmlTextNodes,
|
|
2195
2439
|
normalizeAzureLanguage,
|
|
2196
2440
|
parseSsml,
|
|
2441
|
+
splitSsmlDocument,
|
|
2197
2442
|
validateAzureSsml,
|
|
2198
2443
|
validateSsml,
|
|
2199
2444
|
validateSsmlStructureIntegrity
|