ssml-builder-js 2.11.0 → 2.12.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/dist/core.d.mts CHANGED
@@ -188,6 +188,14 @@ 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
+ /**
192
+ * Splits SSML into independently synthesizable documents without breaking XML
193
+ * elements. Parent elements such as `<voice>` and `<prosody>` are copied into
194
+ * every block. Paragraph and sentence elements therefore remain the natural
195
+ * split points, while oversized text is split at word/character boundaries.
196
+ */
197
+ declare function splitSsmlDocument(ssml: string, maxLength?: number): string[];
198
+
191
199
  declare function parseSsml(xmlString: string): SsmlDocument;
192
200
 
193
201
  interface SsmlValidationError {
@@ -286,6 +294,8 @@ interface AzureValidationOptions {
286
294
  customVoiceDefinitions?: readonly AzureVoiceDefinition[];
287
295
  languageAliases?: Record<string, string | readonly string[]>;
288
296
  maxLength?: number;
297
+ /** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
298
+ maxXmlDepth?: number;
289
299
  /** Optional model identifier used to validate a voice's model compatibility. */
290
300
  model?: string;
291
301
  normalizeLanguage?: (lang: string) => string;
@@ -324,4 +334,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
324
334
  /** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
325
335
  declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
326
336
 
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 };
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 };
package/dist/core.d.ts CHANGED
@@ -188,6 +188,14 @@ 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
+ /**
192
+ * Splits SSML into independently synthesizable documents without breaking XML
193
+ * elements. Parent elements such as `<voice>` and `<prosody>` are copied into
194
+ * every block. Paragraph and sentence elements therefore remain the natural
195
+ * split points, while oversized text is split at word/character boundaries.
196
+ */
197
+ declare function splitSsmlDocument(ssml: string, maxLength?: number): string[];
198
+
191
199
  declare function parseSsml(xmlString: string): SsmlDocument;
192
200
 
193
201
  interface SsmlValidationError {
@@ -286,6 +294,8 @@ interface AzureValidationOptions {
286
294
  customVoiceDefinitions?: readonly AzureVoiceDefinition[];
287
295
  languageAliases?: Record<string, string | readonly string[]>;
288
296
  maxLength?: number;
297
+ /** Maximum XML element nesting depth, counting `<speak>` as depth 1. */
298
+ maxXmlDepth?: number;
289
299
  /** Optional model identifier used to validate a voice's model compatibility. */
290
300
  model?: string;
291
301
  normalizeLanguage?: (lang: string) => string;
@@ -324,4 +334,4 @@ declare function getAzureVoiceCatalogMetadata(): AzureVoiceCatalogMetadata;
324
334
  /** Alias for consumers that refer to the bundled catalog as the built-in catalog. */
325
335
  declare const getBuiltInVoiceCatalogMetadata: typeof getAzureVoiceCatalogMetadata;
326
336
 
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 };
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 };
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,104 @@ 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 splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH) {
1049
+ if (!Number.isInteger(maxLength) || maxLength <= 0) {
1050
+ throw new RangeError("maxLength must be a positive integer");
1051
+ }
1052
+ const document = parseSsml(ssml);
1053
+ if (ssml.length <= maxLength) return [ssml];
1054
+ const children = document.children ?? [];
1055
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength));
1056
+ const chunks = [];
1057
+ let group = [];
1058
+ for (const child of splitChildren) {
1059
+ const candidate = [...group, child];
1060
+ if (documentWithChildren(document, candidate).length <= maxLength) {
1061
+ group = candidate;
1062
+ continue;
1063
+ }
1064
+ if (group.length > 0) chunks.push(group);
1065
+ group = [child];
1066
+ if (documentWithChildren(document, group).length > maxLength) {
1067
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1068
+ }
1069
+ }
1070
+ if (group.length > 0) chunks.push(group);
1071
+ return chunks.map((chunk) => documentWithChildren(document, chunk));
1072
+ }
1073
+
975
1074
  // packages/ssml-core/src/validation.ts
976
1075
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
977
1076
  function validateSsml(xmlString) {
@@ -1666,6 +1765,7 @@ function tokenizeElements(source) {
1666
1765
  attributes,
1667
1766
  childElementIndex,
1668
1767
  end,
1768
+ depth: openElements.length + 1,
1669
1769
  name: tokenName,
1670
1770
  parentName: parent?.name,
1671
1771
  parentVoiceName,
@@ -1856,13 +1956,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1856
1956
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1857
1957
  return;
1858
1958
  }
1959
+ if (parsed.username || parsed.password)
1960
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1859
1961
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1860
1962
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1861
1963
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1862
1964
  try {
1863
- return new URL(allowedOrigin).origin === parsed.origin;
1965
+ const configured = new URL(allowedOrigin);
1966
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
1967
+ return false;
1968
+ return configured.origin === parsed.origin;
1864
1969
  } catch {
1865
- return allowedOrigin === parsed.origin;
1970
+ return false;
1866
1971
  }
1867
1972
  }) ?? false;
1868
1973
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -2070,6 +2175,9 @@ function validateAzureSsml(ssml, options = {}) {
2070
2175
  const maxLength = options.maxLength ?? 1e4;
2071
2176
  if (ssml.length > maxLength)
2072
2177
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
2178
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
2179
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
2180
+ }
2073
2181
  try {
2074
2182
  parseSsml(ssml);
2075
2183
  } catch (error) {
@@ -2079,6 +2187,18 @@ function validateAzureSsml(ssml, options = {}) {
2079
2187
  return diagnostics;
2080
2188
  }
2081
2189
  const tokens = tokenizeElements(ssml);
2190
+ if (options.maxXmlDepth !== void 0) {
2191
+ for (const token of tokens) {
2192
+ if (token.depth > options.maxXmlDepth) {
2193
+ addDiagnostic(
2194
+ diagnostics,
2195
+ ssml,
2196
+ token.start,
2197
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
2198
+ );
2199
+ }
2200
+ }
2201
+ }
2082
2202
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2083
2203
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2084
2204
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -2194,6 +2314,7 @@ var getBuiltInVoiceCatalogMetadata = getAzureVoiceCatalogMetadata;
2194
2314
  mapSsmlTextNodes,
2195
2315
  normalizeAzureLanguage,
2196
2316
  parseSsml,
2317
+ splitSsmlDocument,
2197
2318
  validateAzureSsml,
2198
2319
  validateSsml,
2199
2320
  validateSsmlStructureIntegrity