ssml-builder-js 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/{chunk-4BVNAUVR.mjs → chunk-AQ55MOPU.mjs} +246 -21
- package/dist/chunk-AQ55MOPU.mjs.map +1 -0
- package/dist/chunk-QFIBPCO4.mjs +1816 -0
- package/dist/chunk-QFIBPCO4.mjs.map +1 -0
- package/dist/{chunk-FHDFRM2F.mjs → chunk-UZSCXPC5.mjs} +248 -1722
- package/dist/chunk-UZSCXPC5.mjs.map +1 -0
- package/dist/core.d.mts +62 -3
- package/dist/core.d.ts +62 -3
- package/dist/core.js +246 -20
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +196 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +6 -4
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d-CUXRwSw0.d.mts +232 -0
- package/dist/index.d-CUXRwSw0.d.ts +232 -0
- package/dist/index.d.mts +151 -2
- package/dist/index.d.ts +151 -2
- package/dist/index.js +2264 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +479 -4
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +65 -163
- package/dist/react.d.ts +65 -163
- package/dist/react.js +498 -22
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +162 -18
- package/dist/react.mjs.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-4BVNAUVR.mjs.map +0 -1
- package/dist/chunk-FHDFRM2F.mjs.map +0 -1
package/dist/core.d.ts
CHANGED
|
@@ -188,13 +188,37 @@ 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
|
+
/** Best-effort path to the first source element represented by this chunk. */
|
|
208
|
+
sourceNodePath?: string[];
|
|
209
|
+
}
|
|
210
|
+
interface SplitSsmlOptions {
|
|
211
|
+
maxLength?: number;
|
|
212
|
+
/** Keeps `<mstts:backgroundaudio>` in every chunk instead of the first chunk only. */
|
|
213
|
+
replicateBackgroundAudio?: boolean;
|
|
214
|
+
}
|
|
191
215
|
/**
|
|
192
216
|
* Splits SSML into independently synthesizable documents without breaking XML
|
|
193
217
|
* elements. Parent elements such as `<voice>` and `<prosody>` are copied into
|
|
194
218
|
* every block. Paragraph and sentence elements therefore remain the natural
|
|
195
219
|
* split points, while oversized text is split at word/character boundaries.
|
|
196
220
|
*/
|
|
197
|
-
declare function splitSsmlDocument(ssml: string, maxLength?: number):
|
|
221
|
+
declare function splitSsmlDocument(ssml: string, maxLength?: number | SplitSsmlOptions, options?: SplitSsmlOptions): SsmlChunk[];
|
|
198
222
|
|
|
199
223
|
declare function parseSsml(xmlString: string): SsmlDocument;
|
|
200
224
|
|
|
@@ -292,6 +316,17 @@ interface AzureValidationOptions {
|
|
|
292
316
|
allowHttpAudio?: boolean;
|
|
293
317
|
customVoiceStyleMap?: Record<string, readonly string[]>;
|
|
294
318
|
customVoiceDefinitions?: readonly AzureVoiceDefinition[];
|
|
319
|
+
/** Host-side validation hook for URL-bearing SSML attributes. */
|
|
320
|
+
urlValidator?: AzureUrlValidator;
|
|
321
|
+
/** Alias for urlValidator retained for applications that use the longer name. */
|
|
322
|
+
customUrlValidator?: AzureUrlValidator;
|
|
323
|
+
/** Controls deduplication, caching, cancellation, and concurrency for URL checks. */
|
|
324
|
+
urlValidation?: AzureUrlValidationRunnerOptions;
|
|
325
|
+
/** Flat aliases retained for callers that prefer not to nest URL runner options. */
|
|
326
|
+
urlValidatorConcurrency?: number;
|
|
327
|
+
urlValidatorTimeoutMs?: number;
|
|
328
|
+
urlValidatorSignal?: AbortSignal;
|
|
329
|
+
urlValidatorCache?: Map<string, AzureUrlValidationResult>;
|
|
295
330
|
languageAliases?: Record<string, string | readonly string[]>;
|
|
296
331
|
maxLength?: number;
|
|
297
332
|
/** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
|
|
@@ -311,6 +346,22 @@ interface AzureValidationOptions {
|
|
|
311
346
|
voiceCatalog?: readonly AzureVoiceDefinition[];
|
|
312
347
|
voiceDefinitions?: readonly AzureVoiceDefinition[];
|
|
313
348
|
}
|
|
349
|
+
type AzureUrlValidationResult = boolean | {
|
|
350
|
+
valid: boolean;
|
|
351
|
+
reason?: string;
|
|
352
|
+
};
|
|
353
|
+
type AzureUrlValidator = (url: string, context: {
|
|
354
|
+
tag: string;
|
|
355
|
+
attribute: string;
|
|
356
|
+
}) => AzureUrlValidationResult | Promise<AzureUrlValidationResult>;
|
|
357
|
+
interface AzureUrlValidationRunnerOptions {
|
|
358
|
+
concurrency?: number;
|
|
359
|
+
cache?: Map<string, AzureUrlValidationResult>;
|
|
360
|
+
signal?: AbortSignal;
|
|
361
|
+
timeoutMs?: number;
|
|
362
|
+
}
|
|
363
|
+
/** Wraps a URL validator with URL deduplication, bounded concurrency, caching, and cancellation. */
|
|
364
|
+
declare function createAzureUrlValidatorRunner(validator: AzureUrlValidator, options?: AzureUrlValidationRunnerOptions): AzureUrlValidator;
|
|
314
365
|
type AzureLanguageNormalizationOptions = Pick<AzureValidationOptions, "languageAliases" | "normalizeLanguage">;
|
|
315
366
|
/** @deprecated Use AzureValidationOptions instead. */
|
|
316
367
|
type AzureSsmlValidationOptions = AzureValidationOptions;
|
|
@@ -320,7 +371,15 @@ declare function isValidAzureAudioDuration(value: string): boolean;
|
|
|
320
371
|
declare function normalizeAzureLanguage(language: string, options?: AzureLanguageNormalizationOptions): string;
|
|
321
372
|
/** Compares two Azure language tags after BCP 47 and alias normalization. */
|
|
322
373
|
declare function areAzureLanguagesEquivalent(first: string, second: string, options?: AzureLanguageNormalizationOptions): boolean;
|
|
323
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Validates Azure SSML synchronously unless a URL validator is supplied. URL
|
|
376
|
+
* validation is asynchronous-capable so hosts can perform DNS/private-network checks.
|
|
377
|
+
*/
|
|
378
|
+
declare function validateAzureSsml(ssml: string, options?: Omit<AzureValidationOptions, "urlValidator" | "customUrlValidator">): SsmlDiagnostic[];
|
|
379
|
+
declare function validateAzureSsml(ssml: string, options: AzureValidationOptions & {
|
|
380
|
+
urlValidator?: AzureUrlValidator;
|
|
381
|
+
customUrlValidator?: AzureUrlValidator;
|
|
382
|
+
}): SsmlDiagnostic[] | Promise<SsmlDiagnostic[]>;
|
|
324
383
|
|
|
325
384
|
interface AzureVoiceCatalogMetadata {
|
|
326
385
|
apiVersion: string;
|
|
@@ -334,4 +393,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
|
|
|
334
393
|
/** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
|
|
335
394
|
declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
|
|
336
395
|
|
|
337
|
-
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, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
|
|
396
|
+
export { type AudioElement, type AzureDiagnosticCode, type AzureLanguageNormalizationOptions, type AzureSsmlValidationOptions, type AzureUrlValidationResult, type AzureUrlValidationRunnerOptions, 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, createAzureUrlValidatorRunner, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity };
|
package/dist/core.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(core_exports, {
|
|
|
30
30
|
areAzureLanguagesEquivalent: () => areAzureLanguagesEquivalent,
|
|
31
31
|
buildPartialSsml: () => buildPartialSsml,
|
|
32
32
|
buildSsml: () => buildSsml,
|
|
33
|
+
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
33
34
|
extractSsmlText: () => extractSsmlText,
|
|
34
35
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
35
36
|
fromPlainTextToSsml: () => fromPlainTextToSsml,
|
|
@@ -1045,30 +1046,138 @@ function splitNode(document, node, maxLength, context = []) {
|
|
|
1045
1046
|
flush();
|
|
1046
1047
|
return parts;
|
|
1047
1048
|
}
|
|
1048
|
-
function
|
|
1049
|
-
if (
|
|
1049
|
+
function textFromNode(node) {
|
|
1050
|
+
if (typeof node === "string") return node;
|
|
1051
|
+
if (node.type === "text") return node.value;
|
|
1052
|
+
return (node.children ?? []).map(textFromNode).join("");
|
|
1053
|
+
}
|
|
1054
|
+
function collectMarks(node, marks) {
|
|
1055
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1056
|
+
if (node.type === "mark" && node.name) marks.push(node.name);
|
|
1057
|
+
if (node.type === "bookmark" && node.mark) marks.push(node.mark);
|
|
1058
|
+
for (const child of node.children ?? []) collectMarks(child, marks);
|
|
1059
|
+
}
|
|
1060
|
+
function collectInheritedContext(nodes) {
|
|
1061
|
+
const context = {};
|
|
1062
|
+
const visit = (node) => {
|
|
1063
|
+
if (typeof node === "string" || node.type === "text") return;
|
|
1064
|
+
if (context.voice === void 0 && node.type === "voice" && node.name) context.voice = node.name;
|
|
1065
|
+
if (context.lang === void 0 && node.type === "lang" && node.lang) context.lang = node.lang;
|
|
1066
|
+
if (context.prosody === void 0 && node.type === "prosody") {
|
|
1067
|
+
const prosody = {};
|
|
1068
|
+
for (const [key, value] of Object.entries(node.attributes ?? {})) prosody[key] = String(value);
|
|
1069
|
+
for (const key of ["rate", "pitch", "volume", "contour", "range"]) {
|
|
1070
|
+
const value = node[key];
|
|
1071
|
+
if (value !== void 0) prosody[key] = String(value);
|
|
1072
|
+
}
|
|
1073
|
+
if (Object.keys(prosody).length > 0) context.prosody = prosody;
|
|
1074
|
+
}
|
|
1075
|
+
for (const child of node.children ?? []) visit(child);
|
|
1076
|
+
};
|
|
1077
|
+
nodes.forEach(visit);
|
|
1078
|
+
return context;
|
|
1079
|
+
}
|
|
1080
|
+
function elementName(node) {
|
|
1081
|
+
return node.type === "custom" || node.type === "element" ? node.name : node.type;
|
|
1082
|
+
}
|
|
1083
|
+
function findSourceNodePath(nodes, targetOffset) {
|
|
1084
|
+
let textOffset = 0;
|
|
1085
|
+
let firstPath;
|
|
1086
|
+
let foundPath;
|
|
1087
|
+
const visit = (node, path) => {
|
|
1088
|
+
if (typeof node === "string" || node.type === "text") {
|
|
1089
|
+
const text = typeof node === "string" ? node : node.value;
|
|
1090
|
+
if (text && firstPath === void 0) firstPath = [...path];
|
|
1091
|
+
if (text && foundPath === void 0 && targetOffset < textOffset + text.length) foundPath = [...path];
|
|
1092
|
+
textOffset += text.length;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
node.children?.forEach((child, index) => {
|
|
1096
|
+
const childPath = typeof child === "string" || child.type === "text" ? path : [...path, `${elementName(child)}[${index}]`];
|
|
1097
|
+
visit(child, childPath);
|
|
1098
|
+
});
|
|
1099
|
+
};
|
|
1100
|
+
nodes.forEach((node, index) => {
|
|
1101
|
+
if (!foundPath) {
|
|
1102
|
+
if (typeof node === "string" || node.type === "text") visit(node, ["speak"]);
|
|
1103
|
+
else visit(node, ["speak", `${elementName(node)}[${index}]`]);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
return foundPath ?? firstPath;
|
|
1107
|
+
}
|
|
1108
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1109
|
+
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1110
|
+
const text = nodes.map(textFromNode).join("");
|
|
1111
|
+
const marks = [];
|
|
1112
|
+
for (const node of nodes) collectMarks(node, marks);
|
|
1113
|
+
const inheritedContext = collectInheritedContext(nodes);
|
|
1114
|
+
if (inheritedContext.lang === void 0 && document.lang) inheritedContext.lang = document.lang;
|
|
1115
|
+
return {
|
|
1116
|
+
chunkIndex,
|
|
1117
|
+
ssml: documentWithChildren(document, chunkNodes),
|
|
1118
|
+
originalTextRange: { start: textStart, end: textStart + text.length },
|
|
1119
|
+
inheritedContext,
|
|
1120
|
+
containedMarks: marks,
|
|
1121
|
+
hasBackgroundAudio: chunkNodes.some(
|
|
1122
|
+
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1123
|
+
),
|
|
1124
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
1128
|
+
const resolvedMaxLength = typeof maxLength === "number" ? maxLength : maxLength.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
1129
|
+
const resolvedOptions = typeof maxLength === "number" ? options : maxLength;
|
|
1130
|
+
if (!Number.isInteger(resolvedMaxLength) || resolvedMaxLength <= 0) {
|
|
1050
1131
|
throw new RangeError("maxLength must be a positive integer");
|
|
1051
1132
|
}
|
|
1052
1133
|
const document = parseSsml(ssml);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1134
|
+
const backgroundAudio = (document.children ?? []).find(
|
|
1135
|
+
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1136
|
+
);
|
|
1137
|
+
if (ssml.length <= resolvedMaxLength) {
|
|
1138
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1139
|
+
}
|
|
1140
|
+
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1141
|
+
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
1142
|
+
const backgroundDocumentLength = backgroundAudio ? documentWithChildren(document, [backgroundAudio]).length : plainDocumentLength;
|
|
1143
|
+
const backgroundOverhead = Math.max(0, backgroundDocumentLength - plainDocumentLength);
|
|
1144
|
+
const contentMaxLength = Math.max(1, resolvedMaxLength - backgroundOverhead);
|
|
1145
|
+
const splitChildren = contentChildren.flatMap((child) => splitNode(document, child, contentMaxLength));
|
|
1056
1146
|
const chunks = [];
|
|
1057
1147
|
let group = [];
|
|
1058
1148
|
for (const child of splitChildren) {
|
|
1059
1149
|
const candidate = [...group, child];
|
|
1060
|
-
if (documentWithChildren(document, candidate).length <=
|
|
1150
|
+
if (documentWithChildren(document, candidate).length <= contentMaxLength) {
|
|
1061
1151
|
group = candidate;
|
|
1062
1152
|
continue;
|
|
1063
1153
|
}
|
|
1064
1154
|
if (group.length > 0) chunks.push(group);
|
|
1065
1155
|
group = [child];
|
|
1066
|
-
if (documentWithChildren(document, group).length >
|
|
1156
|
+
if (documentWithChildren(document, group).length > contentMaxLength) {
|
|
1067
1157
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1068
1158
|
}
|
|
1069
1159
|
}
|
|
1070
1160
|
if (group.length > 0) chunks.push(group);
|
|
1071
|
-
|
|
1161
|
+
if (chunks.length === 0) {
|
|
1162
|
+
const result = createChunk(document, [], 0, 0, backgroundAudio, resolvedOptions.replicateBackgroundAudio ?? false);
|
|
1163
|
+
if (result.ssml.length > resolvedMaxLength) {
|
|
1164
|
+
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1165
|
+
}
|
|
1166
|
+
return [result];
|
|
1167
|
+
}
|
|
1168
|
+
let textStart = 0;
|
|
1169
|
+
return chunks.map((chunk, chunkIndex) => {
|
|
1170
|
+
const result = createChunk(
|
|
1171
|
+
document,
|
|
1172
|
+
chunk,
|
|
1173
|
+
chunkIndex,
|
|
1174
|
+
textStart,
|
|
1175
|
+
backgroundAudio,
|
|
1176
|
+
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1177
|
+
);
|
|
1178
|
+
textStart = result.originalTextRange.end;
|
|
1179
|
+
return result;
|
|
1180
|
+
});
|
|
1072
1181
|
}
|
|
1073
1182
|
|
|
1074
1183
|
// packages/ssml-core/src/validation.ts
|
|
@@ -1238,7 +1347,7 @@ async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
|
1238
1347
|
|
|
1239
1348
|
// packages/ssml-core/src/migration.ts
|
|
1240
1349
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1241
|
-
function
|
|
1350
|
+
function elementName2(element) {
|
|
1242
1351
|
switch (element.type) {
|
|
1243
1352
|
case "custom":
|
|
1244
1353
|
case "element":
|
|
@@ -1391,7 +1500,7 @@ function extractSsmlTranslatableText(ssml, options = {}) {
|
|
|
1391
1500
|
}
|
|
1392
1501
|
return;
|
|
1393
1502
|
}
|
|
1394
|
-
const tag =
|
|
1503
|
+
const tag = elementName2(node);
|
|
1395
1504
|
if (skipTags.has(tag.toLowerCase())) return;
|
|
1396
1505
|
visit(childrenOf(node), [...ancestors, tag], [...path, String(index)]);
|
|
1397
1506
|
});
|
|
@@ -1437,7 +1546,7 @@ function serializeDocument2(document) {
|
|
|
1437
1546
|
const serialize = (node) => {
|
|
1438
1547
|
if (typeof node === "string") return node.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1439
1548
|
if (node.type === "text") return serialize(node.value);
|
|
1440
|
-
const tag =
|
|
1549
|
+
const tag = elementName2(node);
|
|
1441
1550
|
const nodeAttributes = elementAttributes(node);
|
|
1442
1551
|
const serializedAttributes = Object.entries(nodeAttributes).map(([name, value]) => ` ${name}="${serialize(value).replace(/"/g, """)}"`).join("");
|
|
1443
1552
|
const children = childrenOf(node).map(serialize).join("");
|
|
@@ -1451,7 +1560,7 @@ function flatten(document) {
|
|
|
1451
1560
|
nodes.forEach((node, index) => {
|
|
1452
1561
|
if (typeof node === "string" || node.type === "text") return;
|
|
1453
1562
|
const currentPath = `${path}/${index}`;
|
|
1454
|
-
result.push({ name:
|
|
1563
|
+
result.push({ name: elementName2(node), attributes: elementAttributes(node), path: currentPath });
|
|
1455
1564
|
visit(childrenOf(node), currentPath);
|
|
1456
1565
|
});
|
|
1457
1566
|
};
|
|
@@ -1649,6 +1758,69 @@ var AZURE_VOICE_DEFINITIONS = [
|
|
|
1649
1758
|
];
|
|
1650
1759
|
|
|
1651
1760
|
// packages/ssml-core/src/azureValidation.ts
|
|
1761
|
+
function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
1762
|
+
if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
|
|
1763
|
+
const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
|
|
1764
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
1765
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1766
|
+
const waiters = [];
|
|
1767
|
+
let active = 0;
|
|
1768
|
+
const acquire = async () => {
|
|
1769
|
+
if (active < concurrency) {
|
|
1770
|
+
active += 1;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
1774
|
+
active += 1;
|
|
1775
|
+
};
|
|
1776
|
+
const release = () => {
|
|
1777
|
+
active -= 1;
|
|
1778
|
+
waiters.shift()?.();
|
|
1779
|
+
};
|
|
1780
|
+
const check = async (url, context) => {
|
|
1781
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1782
|
+
const cached = cache.get(url);
|
|
1783
|
+
if (cached !== void 0) return cached;
|
|
1784
|
+
const existing = inFlight.get(url);
|
|
1785
|
+
if (existing) return existing;
|
|
1786
|
+
const promise = (async () => {
|
|
1787
|
+
await acquire();
|
|
1788
|
+
try {
|
|
1789
|
+
if (options.signal?.aborted) throw new Error("URL validation was aborted.");
|
|
1790
|
+
const validation = Promise.resolve(validator(url, context));
|
|
1791
|
+
let timer;
|
|
1792
|
+
let abortHandler;
|
|
1793
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1794
|
+
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1795
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1796
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1797
|
+
timer = setTimeout(
|
|
1798
|
+
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
1799
|
+
options.timeoutMs
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
try {
|
|
1804
|
+
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1805
|
+
cache.set(url, result);
|
|
1806
|
+
return result;
|
|
1807
|
+
} finally {
|
|
1808
|
+
if (timer) clearTimeout(timer);
|
|
1809
|
+
if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
|
|
1810
|
+
}
|
|
1811
|
+
} finally {
|
|
1812
|
+
release();
|
|
1813
|
+
}
|
|
1814
|
+
})();
|
|
1815
|
+
inFlight.set(url, promise);
|
|
1816
|
+
try {
|
|
1817
|
+
return await promise;
|
|
1818
|
+
} finally {
|
|
1819
|
+
inFlight.delete(url);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
return (url, context) => check(url, context);
|
|
1823
|
+
}
|
|
1652
1824
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1653
1825
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
1654
1826
|
"characters",
|
|
@@ -1943,23 +2115,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
|
|
|
1943
2115
|
);
|
|
1944
2116
|
}
|
|
1945
2117
|
}
|
|
1946
|
-
function validateAudioSource(token, source, diagnostics, options,
|
|
2118
|
+
function validateAudioSource(token, source, diagnostics, options, elementName3) {
|
|
1947
2119
|
const src = attr(token, "src");
|
|
1948
2120
|
if (!src) {
|
|
1949
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2121
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
|
|
1950
2122
|
return;
|
|
1951
2123
|
}
|
|
1952
2124
|
let parsed;
|
|
1953
2125
|
try {
|
|
1954
2126
|
parsed = new URL(src);
|
|
1955
2127
|
} catch {
|
|
1956
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2128
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
|
|
1957
2129
|
return;
|
|
1958
2130
|
}
|
|
1959
2131
|
if (parsed.username || parsed.password)
|
|
1960
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2132
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
|
|
1961
2133
|
if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
|
|
1962
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2134
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
|
|
1963
2135
|
const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
|
|
1964
2136
|
try {
|
|
1965
2137
|
const configured = new URL(allowedOrigin);
|
|
@@ -1971,13 +2143,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
|
|
|
1971
2143
|
}
|
|
1972
2144
|
}) ?? false;
|
|
1973
2145
|
if (options.allowedAudioOrigins && !isAllowedOrigin)
|
|
1974
|
-
addDiagnostic(diagnostics, source, token.start, `<${
|
|
2146
|
+
addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
|
|
1975
2147
|
else if (!isAllowedOrigin && !options.allowExternalAudio)
|
|
1976
2148
|
addDiagnostic(
|
|
1977
2149
|
diagnostics,
|
|
1978
2150
|
source,
|
|
1979
2151
|
token.start,
|
|
1980
|
-
`<${
|
|
2152
|
+
`<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
|
|
1981
2153
|
);
|
|
1982
2154
|
}
|
|
1983
2155
|
function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
|
|
@@ -2159,7 +2331,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
|
|
|
2159
2331
|
addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
|
|
2160
2332
|
}
|
|
2161
2333
|
}
|
|
2162
|
-
function
|
|
2334
|
+
function validateAzureSsmlStatic(ssml, options = {}) {
|
|
2163
2335
|
const diagnostics = [];
|
|
2164
2336
|
if (typeof ssml !== "string") {
|
|
2165
2337
|
return [
|
|
@@ -2283,6 +2455,59 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2283
2455
|
}
|
|
2284
2456
|
return diagnostics;
|
|
2285
2457
|
}
|
|
2458
|
+
function urlAttributes(token) {
|
|
2459
|
+
const tag = canonicalTagName(token.name);
|
|
2460
|
+
const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
|
|
2461
|
+
return attributes.flatMap((attribute) => {
|
|
2462
|
+
const value = attr(token, attribute);
|
|
2463
|
+
return value === void 0 ? [] : [{ attribute, value }];
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
function validateAzureSsml(ssml, options = {}) {
|
|
2467
|
+
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
2468
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
2469
|
+
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
2470
|
+
const runnerOptions = options.urlValidation ?? {};
|
|
2471
|
+
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
2472
|
+
...runnerOptions,
|
|
2473
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
2474
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
2475
|
+
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2476
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2477
|
+
});
|
|
2478
|
+
let tokens;
|
|
2479
|
+
try {
|
|
2480
|
+
tokens = tokenizeElements(ssml);
|
|
2481
|
+
} catch {
|
|
2482
|
+
return diagnostics;
|
|
2483
|
+
}
|
|
2484
|
+
const checks = tokens.flatMap(
|
|
2485
|
+
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2486
|
+
try {
|
|
2487
|
+
const result = await boundedValidator(value, { tag: token.name, attribute });
|
|
2488
|
+
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2489
|
+
if (!valid) {
|
|
2490
|
+
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
2491
|
+
addDiagnostic(
|
|
2492
|
+
diagnostics,
|
|
2493
|
+
ssml,
|
|
2494
|
+
token.start,
|
|
2495
|
+
`<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2500
|
+
addDiagnostic(
|
|
2501
|
+
diagnostics,
|
|
2502
|
+
ssml,
|
|
2503
|
+
token.start,
|
|
2504
|
+
`<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
})
|
|
2508
|
+
);
|
|
2509
|
+
return Promise.all(checks).then(() => diagnostics);
|
|
2510
|
+
}
|
|
2286
2511
|
|
|
2287
2512
|
// packages/ssml-core/src/generated/azureVoiceCatalog.ts
|
|
2288
2513
|
var AZURE_VOICE_CATALOG_METADATA = {
|
|
@@ -2305,6 +2530,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
|
|
|
2305
2530
|
areAzureLanguagesEquivalent,
|
|
2306
2531
|
buildPartialSsml,
|
|
2307
2532
|
buildSsml,
|
|
2533
|
+
createAzureUrlValidatorRunner,
|
|
2308
2534
|
extractSsmlText,
|
|
2309
2535
|
extractSsmlTranslatableText,
|
|
2310
2536
|
fromPlainTextToSsml,
|