ssml-builder-js 2.10.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/elements.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  resolveExpressAsStyles,
37
37
  updateEditableText,
38
38
  validateAzureSsml
39
- } from "./chunk-BYIZQL2W.mjs";
39
+ } from "./chunk-FHDFRM2F.mjs";
40
40
  import "./chunk-6S5ODO6A.mjs";
41
41
 
42
42
  // packages/ssml-editor-react/src/ssmlInsertions.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceCatalogMetadata, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, ExtractSsmlTranslatableTextOptions, FromPlainTextToSsmlOptions, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsAudioDurationElement, MsttsEmbeddingElement, MsttsSilenceElement, MsttsTtsEmbeddingElement, MsttsVisemeElement, MsttsVoiceConversionElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBackgroundAudioNode, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDiagnosticSource, SsmlDialogNode, SsmlDocument, SsmlElement, SsmlElementBase, SsmlEmbeddingNode, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlStructureIntegrityResult, SsmlStructureMismatch, SsmlText, SsmlTextNodeContext, SsmlTtsEmbeddingNode, SsmlTurnNode, SsmlValidationError, SsmlVoiceConversionNode, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity } from './core.mjs';
1
+ export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceCatalogMetadata, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, ExtractSsmlTranslatableTextOptions, FromPlainTextToSsmlOptions, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsAudioDurationElement, MsttsEmbeddingElement, MsttsSilenceElement, MsttsTtsEmbeddingElement, MsttsVisemeElement, MsttsVoiceConversionElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBackgroundAudioNode, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDiagnosticSource, SsmlDialogNode, SsmlDocument, SsmlElement, SsmlElementBase, SsmlEmbeddingNode, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlStructureIntegrityResult, SsmlStructureMismatch, SsmlText, SsmlTextNodeContext, SsmlTtsEmbeddingNode, SsmlTurnNode, SsmlValidationError, SsmlVoiceConversionNode, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity } from './core.mjs';
2
2
 
3
3
  interface TtsConfig {
4
4
  signal?: AbortSignal;
@@ -8,6 +8,31 @@ interface TtsConfig {
8
8
  region: string;
9
9
  outputFormat?: string;
10
10
  }
11
+ interface SsmlSynthesisBoundary {
12
+ text: string;
13
+ audioOffsetMs: number;
14
+ durationMs: number;
15
+ }
16
+ interface SsmlSynthesisViseme {
17
+ visemeId: number;
18
+ audioOffsetMs: number;
19
+ }
20
+ interface SsmlSynthesisBookmark {
21
+ name: string;
22
+ audioOffsetMs: number;
23
+ }
24
+ /** Audio and Azure Speech synchronization events emitted for one SSML request. */
25
+ interface SsmlSynthesisResult {
26
+ audioData: ArrayBuffer;
27
+ durationMs: number;
28
+ boundaries?: SsmlSynthesisBoundary[];
29
+ /** Alias matching the Azure Speech event name. */
30
+ wordBoundary?: SsmlSynthesisBoundary[];
31
+ /** Alias for consumers that use Azure's word-boundary terminology. */
32
+ wordBoundaries?: SsmlSynthesisBoundary[];
33
+ visemes?: SsmlSynthesisViseme[];
34
+ bookmarks?: SsmlSynthesisBookmark[];
35
+ }
11
36
  interface AzureTtsLogger {
12
37
  debug?: (...args: unknown[]) => void;
13
38
  info?: (...args: unknown[]) => void;
@@ -40,8 +65,36 @@ declare class AzureTtsClient {
40
65
  #private;
41
66
  constructor(options: AzureTtsClientOptions);
42
67
  synthesize(ssml: string): Promise<ArrayBuffer>;
68
+ synthesizeSsml(ssml: string): Promise<SsmlSynthesisResult>;
43
69
  }
44
70
 
71
+ declare function synthesizeSsml(ssml: string, config: TtsConfig): Promise<SsmlSynthesisResult>;
72
+ /** Backward-compatible audio-only synthesis helper. */
45
73
  declare function synthesizeSpeech(ssml: string, config: TtsConfig): Promise<ArrayBuffer>;
46
74
 
47
- export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type TtsConfig, synthesizeSpeech };
75
+ interface FetchAzureVoiceCatalogOptions {
76
+ apiKey: string;
77
+ region: string | string[];
78
+ }
79
+ interface AzureVoiceCatalogVoice {
80
+ name: string;
81
+ locale: string;
82
+ secondaryLocales?: readonly string[];
83
+ styles?: readonly string[];
84
+ regions: readonly string[];
85
+ status?: "ga" | "preview" | "deprecated";
86
+ }
87
+ interface FetchedAzureVoiceCatalogMetadata {
88
+ voiceCount: number;
89
+ generatedAt: string;
90
+ apiVersion: string;
91
+ regions: readonly string[];
92
+ }
93
+ interface AzureVoiceCatalog {
94
+ voices: readonly AzureVoiceCatalogVoice[];
95
+ metadata: FetchedAzureVoiceCatalogMetadata;
96
+ }
97
+ /** Fetches and deduplicates the current Azure Speech voice catalog for one or more regions. */
98
+ declare function fetchAzureVoiceCatalog(options: FetchAzureVoiceCatalogOptions): Promise<AzureVoiceCatalog>;
99
+
100
+ export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type AzureVoiceCatalog, type AzureVoiceCatalogVoice, type FetchAzureVoiceCatalogOptions, type FetchedAzureVoiceCatalogMetadata, type SsmlSynthesisBookmark, type SsmlSynthesisBoundary, type SsmlSynthesisResult, type SsmlSynthesisViseme, type TtsConfig, fetchAzureVoiceCatalog, synthesizeSpeech, synthesizeSsml };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceCatalogMetadata, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, ExtractSsmlTranslatableTextOptions, FromPlainTextToSsmlOptions, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsAudioDurationElement, MsttsEmbeddingElement, MsttsSilenceElement, MsttsTtsEmbeddingElement, MsttsVisemeElement, MsttsVoiceConversionElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBackgroundAudioNode, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDiagnosticSource, SsmlDialogNode, SsmlDocument, SsmlElement, SsmlElementBase, SsmlEmbeddingNode, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlStructureIntegrityResult, SsmlStructureMismatch, SsmlText, SsmlTextNodeContext, SsmlTtsEmbeddingNode, SsmlTurnNode, SsmlValidationError, SsmlVoiceConversionNode, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity } from './core.js';
1
+ export { AudioElement, AzureDiagnosticCode, AzureLanguageNormalizationOptions, AzureSsmlValidationOptions, AzureValidationOptions, AzureVoiceCatalogMetadata, AzureVoiceDefinition, AzureVoiceMetadata, BookmarkElement, BreakElement, BuildPartialSsmlOptions, CustomElement, EmphasisElement, ExpressAsElement, ExtractSsmlTranslatableTextOptions, FromPlainTextToSsmlOptions, LangElement, LexiconElement, MapSsmlTextNodesOptions, MarkElement, MsttsAudioDurationElement, MsttsEmbeddingElement, MsttsSilenceElement, MsttsTtsEmbeddingElement, MsttsVisemeElement, MsttsVoiceConversionElement, NamedElement, ParagraphElement, PhonemeElement, ProsodyElement, SayAsElement, SentenceElement, SsmlAttributeValue, SsmlAttributes, SsmlBackgroundAudioNode, SsmlBreakElement, SsmlDiagnostic, SsmlDiagnosticSeverity, SsmlDiagnosticSource, SsmlDialogNode, SsmlDocument, SsmlElement, SsmlElementBase, SsmlEmbeddingNode, SsmlExpressAsElement, SsmlNode, SsmlPartialContext, SsmlPartialProsody, SsmlPartialVoice, SsmlPhonemeElement, SsmlProsodyElement, SsmlSayAsElement, SsmlStructureIntegrityResult, SsmlStructureMismatch, SsmlText, SsmlTextNodeContext, SsmlTtsEmbeddingNode, SsmlTurnNode, SsmlValidationError, SsmlVoiceConversionNode, SsmlVoiceElement, SubElement, VoiceElement, WordElement, areAzureLanguagesEquivalent, buildPartialSsml, buildSsml, extractSsmlText, extractSsmlTranslatableText, fromPlainTextToSsml, getAzureVoiceCatalogMetadata, getBuiltInVoiceCatalogMetadata, isValidAzureAudioDuration, mapSsmlTextNodes, normalizeAzureLanguage, parseSsml, splitSsmlDocument, validateAzureSsml, validateSsml, validateSsmlStructureIntegrity } from './core.js';
2
2
 
3
3
  interface TtsConfig {
4
4
  signal?: AbortSignal;
@@ -8,6 +8,31 @@ interface TtsConfig {
8
8
  region: string;
9
9
  outputFormat?: string;
10
10
  }
11
+ interface SsmlSynthesisBoundary {
12
+ text: string;
13
+ audioOffsetMs: number;
14
+ durationMs: number;
15
+ }
16
+ interface SsmlSynthesisViseme {
17
+ visemeId: number;
18
+ audioOffsetMs: number;
19
+ }
20
+ interface SsmlSynthesisBookmark {
21
+ name: string;
22
+ audioOffsetMs: number;
23
+ }
24
+ /** Audio and Azure Speech synchronization events emitted for one SSML request. */
25
+ interface SsmlSynthesisResult {
26
+ audioData: ArrayBuffer;
27
+ durationMs: number;
28
+ boundaries?: SsmlSynthesisBoundary[];
29
+ /** Alias matching the Azure Speech event name. */
30
+ wordBoundary?: SsmlSynthesisBoundary[];
31
+ /** Alias for consumers that use Azure's word-boundary terminology. */
32
+ wordBoundaries?: SsmlSynthesisBoundary[];
33
+ visemes?: SsmlSynthesisViseme[];
34
+ bookmarks?: SsmlSynthesisBookmark[];
35
+ }
11
36
  interface AzureTtsLogger {
12
37
  debug?: (...args: unknown[]) => void;
13
38
  info?: (...args: unknown[]) => void;
@@ -40,8 +65,36 @@ declare class AzureTtsClient {
40
65
  #private;
41
66
  constructor(options: AzureTtsClientOptions);
42
67
  synthesize(ssml: string): Promise<ArrayBuffer>;
68
+ synthesizeSsml(ssml: string): Promise<SsmlSynthesisResult>;
43
69
  }
44
70
 
71
+ declare function synthesizeSsml(ssml: string, config: TtsConfig): Promise<SsmlSynthesisResult>;
72
+ /** Backward-compatible audio-only synthesis helper. */
45
73
  declare function synthesizeSpeech(ssml: string, config: TtsConfig): Promise<ArrayBuffer>;
46
74
 
47
- export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type TtsConfig, synthesizeSpeech };
75
+ interface FetchAzureVoiceCatalogOptions {
76
+ apiKey: string;
77
+ region: string | string[];
78
+ }
79
+ interface AzureVoiceCatalogVoice {
80
+ name: string;
81
+ locale: string;
82
+ secondaryLocales?: readonly string[];
83
+ styles?: readonly string[];
84
+ regions: readonly string[];
85
+ status?: "ga" | "preview" | "deprecated";
86
+ }
87
+ interface FetchedAzureVoiceCatalogMetadata {
88
+ voiceCount: number;
89
+ generatedAt: string;
90
+ apiVersion: string;
91
+ regions: readonly string[];
92
+ }
93
+ interface AzureVoiceCatalog {
94
+ voices: readonly AzureVoiceCatalogVoice[];
95
+ metadata: FetchedAzureVoiceCatalogMetadata;
96
+ }
97
+ /** Fetches and deduplicates the current Azure Speech voice catalog for one or more regions. */
98
+ declare function fetchAzureVoiceCatalog(options: FetchAzureVoiceCatalogOptions): Promise<AzureVoiceCatalog>;
99
+
100
+ export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type AzureVoiceCatalog, type AzureVoiceCatalogVoice, type FetchAzureVoiceCatalogOptions, type FetchedAzureVoiceCatalogMetadata, type SsmlSynthesisBookmark, type SsmlSynthesisBoundary, type SsmlSynthesisResult, type SsmlSynthesisViseme, type TtsConfig, fetchAzureVoiceCatalog, synthesizeSpeech, synthesizeSsml };
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ __export(src_exports, {
45
45
  buildSsml: () => buildSsml,
46
46
  extractSsmlText: () => extractSsmlText,
47
47
  extractSsmlTranslatableText: () => extractSsmlTranslatableText,
48
+ fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
48
49
  fromPlainTextToSsml: () => fromPlainTextToSsml,
49
50
  getAzureVoiceCatalogMetadata: () => getAzureVoiceCatalogMetadata,
50
51
  getBuiltInVoiceCatalogMetadata: () => getBuiltInVoiceCatalogMetadata,
@@ -52,7 +53,9 @@ __export(src_exports, {
52
53
  mapSsmlTextNodes: () => mapSsmlTextNodes,
53
54
  normalizeAzureLanguage: () => normalizeAzureLanguage,
54
55
  parseSsml: () => parseSsml,
56
+ splitSsmlDocument: () => splitSsmlDocument,
55
57
  synthesizeSpeech: () => synthesizeSpeech,
58
+ synthesizeSsml: () => synthesizeSsml,
56
59
  validateAzureSsml: () => validateAzureSsml,
57
60
  validateSsml: () => validateSsml,
58
61
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
@@ -986,6 +989,104 @@ function buildPartialSsml(textOrOptions, context) {
986
989
  return serializePartialSsml(textOrOptions.text, textOrOptions);
987
990
  }
988
991
 
992
+ // packages/ssml-core/src/split.ts
993
+ var DEFAULT_MAX_LENGTH = 1e4;
994
+ function cloneElement(element, children) {
995
+ return { ...element, children };
996
+ }
997
+ function documentWithChildren(document, children) {
998
+ return buildSsml({ ...document, children });
999
+ }
1000
+ function wrapWithContext(node, context) {
1001
+ return context.reduceRight((current, parent) => cloneElement(parent, [current]), node);
1002
+ }
1003
+ function documentWithNode(document, node, context) {
1004
+ return documentWithChildren(document, [wrapWithContext(node, context)]);
1005
+ }
1006
+ function splitTextNode(text, document, context, maxLength) {
1007
+ if (!text) return [text];
1008
+ const parts = [];
1009
+ let start = 0;
1010
+ while (start < text.length) {
1011
+ let end = start + 1;
1012
+ let bestEnd = end;
1013
+ while (end <= text.length) {
1014
+ if (documentWithNode(document, text.slice(start, end), context).length > maxLength) break;
1015
+ bestEnd = end;
1016
+ end += 1;
1017
+ }
1018
+ if (bestEnd === start) {
1019
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1020
+ }
1021
+ const segment = text.slice(start, bestEnd);
1022
+ const boundary = Math.max(segment.lastIndexOf(" "), segment.lastIndexOf("\n"), segment.lastIndexOf(" "));
1023
+ const splitEnd = boundary > 0 ? start + boundary + 1 : bestEnd;
1024
+ parts.push(text.slice(start, splitEnd));
1025
+ start = splitEnd;
1026
+ }
1027
+ return parts;
1028
+ }
1029
+ function splitNode(document, node, maxLength, context = []) {
1030
+ if (typeof node === "string" || node.type === "text") {
1031
+ const value = typeof node === "string" ? node : node.value;
1032
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1033
+ return splitTextNode(value, document, context, maxLength);
1034
+ }
1035
+ if (documentWithNode(document, node, context).length <= maxLength) return [node];
1036
+ const children = node.children ?? [];
1037
+ if (children.length === 0) {
1038
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1039
+ }
1040
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength, [...context, node]));
1041
+ const parts = [];
1042
+ let group = [];
1043
+ const flush = () => {
1044
+ if (group.length > 0) {
1045
+ parts.push(cloneElement(node, group));
1046
+ group = [];
1047
+ }
1048
+ };
1049
+ for (const child of splitChildren) {
1050
+ const candidate = cloneElement(node, [...group, child]);
1051
+ if (documentWithNode(document, candidate, context).length <= maxLength) {
1052
+ group.push(child);
1053
+ continue;
1054
+ }
1055
+ flush();
1056
+ if (documentWithNode(document, cloneElement(node, [child]), context).length > maxLength) {
1057
+ throw new RangeError(`maxLength is too small to contain <${node.type}>`);
1058
+ }
1059
+ group.push(child);
1060
+ }
1061
+ flush();
1062
+ return parts;
1063
+ }
1064
+ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH) {
1065
+ if (!Number.isInteger(maxLength) || maxLength <= 0) {
1066
+ throw new RangeError("maxLength must be a positive integer");
1067
+ }
1068
+ const document = parseSsml(ssml);
1069
+ if (ssml.length <= maxLength) return [ssml];
1070
+ const children = document.children ?? [];
1071
+ const splitChildren = children.flatMap((child) => splitNode(document, child, maxLength));
1072
+ const chunks = [];
1073
+ let group = [];
1074
+ for (const child of splitChildren) {
1075
+ const candidate = [...group, child];
1076
+ if (documentWithChildren(document, candidate).length <= maxLength) {
1077
+ group = candidate;
1078
+ continue;
1079
+ }
1080
+ if (group.length > 0) chunks.push(group);
1081
+ group = [child];
1082
+ if (documentWithChildren(document, group).length > maxLength) {
1083
+ throw new RangeError("maxLength is too small to contain the SSML document wrapper");
1084
+ }
1085
+ }
1086
+ if (group.length > 0) chunks.push(group);
1087
+ return chunks.map((chunk) => documentWithChildren(document, chunk));
1088
+ }
1089
+
989
1090
  // packages/ssml-core/src/validation.ts
990
1091
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
991
1092
  function validateSsml(xmlString) {
@@ -1501,6 +1602,9 @@ var AZURE_VOICE_DEFINITIONS = [
1501
1602
  },
1502
1603
  { name: "es-ES-ElviraNeural", locale: "es-ES" },
1503
1604
  { name: "fil-PH-AngeloNeural", locale: "fil-PH" },
1605
+ { name: "fil-PH-Angelo:DragonHDLatestNeural", locale: "fil-PH" },
1606
+ { name: "fil-PH-BlessicaNeural", locale: "fil-PH" },
1607
+ { name: "fil-PH-Blessica:DragonHDLatestNeural", locale: "fil-PH" },
1504
1608
  { name: "fr-FR-DeniseNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1505
1609
  { name: "fr-FR-HenriNeural", locale: "fr-FR", styles: ["cheerful", "sad"] },
1506
1610
  { name: "id-ID-GadisNeural", locale: "id-ID" },
@@ -1597,6 +1701,18 @@ var ALLOWED_SILENCE_TYPES = /* @__PURE__ */ new Set([
1597
1701
  "Enumerationcomma"
1598
1702
  ]);
1599
1703
  var ALLOWED_VISEME_TYPES = /* @__PURE__ */ new Set(["redlips_front", "FacialExpression"]);
1704
+ var DEFAULT_PREVIEW_TAGS = /* @__PURE__ */ new Set(["mstts:voiceconversion"]);
1705
+ function featureStatusForTag(name, options) {
1706
+ const tagName = canonicalTagName(name);
1707
+ const configured = Object.entries(options.tagStatuses ?? {}).find(
1708
+ ([candidate]) => canonicalTagName(candidate) === tagName
1709
+ )?.[1];
1710
+ if (configured) return configured;
1711
+ if ((options.previewTags ?? [...DEFAULT_PREVIEW_TAGS]).some((candidate) => canonicalTagName(candidate) === tagName))
1712
+ return "preview";
1713
+ if ((options.deprecatedTags ?? []).some((candidate) => canonicalTagName(candidate) === tagName)) return "deprecated";
1714
+ return void 0;
1715
+ }
1600
1716
  function decodeAttribute(value) {
1601
1717
  return value.replace(
1602
1718
  /&(?:amp|apos|gt|lt|quot);/gi,
@@ -1665,6 +1781,7 @@ function tokenizeElements(source) {
1665
1781
  attributes,
1666
1782
  childElementIndex,
1667
1783
  end,
1784
+ depth: openElements.length + 1,
1668
1785
  name: tokenName,
1669
1786
  parentName: parent?.name,
1670
1787
  parentVoiceName,
@@ -1713,9 +1830,9 @@ function isValidAzureAudioDuration(value) {
1713
1830
  return Number(clock[1]) > 0 || Number(clock[2]) > 0 || Number(clock[3]) > 0 || Number(clock[4] ?? 0) > 0;
1714
1831
  }
1715
1832
  function isValidAzureBackgroundAudioDuration(value) {
1716
- const match = /^(\d+(?:\.\d+)?)(ms|s)?$/i.exec(value.trim());
1833
+ const match = /^(\d+)$/.exec(value.trim());
1717
1834
  if (!match) return false;
1718
- const milliseconds = Number(match[1]) * (match[2]?.toLowerCase() === "s" ? 1e3 : 1);
1835
+ const milliseconds = Number(match[1]);
1719
1836
  return Number.isFinite(milliseconds) && milliseconds >= 0 && milliseconds <= 1e4;
1720
1837
  }
1721
1838
  function attr(token, name) {
@@ -1855,13 +1972,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1855
1972
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1856
1973
  return;
1857
1974
  }
1975
+ if (parsed.username || parsed.password)
1976
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1858
1977
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1859
1978
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1860
1979
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1861
1980
  try {
1862
- return new URL(allowedOrigin).origin === parsed.origin;
1981
+ const configured = new URL(allowedOrigin);
1982
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
1983
+ return false;
1984
+ return configured.origin === parsed.origin;
1863
1985
  } catch {
1864
- return allowedOrigin === parsed.origin;
1986
+ return false;
1865
1987
  }
1866
1988
  }) ?? false;
1867
1989
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -1876,6 +1998,25 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1876
1998
  }
1877
1999
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
1878
2000
  const name = token.name.toLowerCase();
2001
+ const tagStatus = featureStatusForTag(token.name, options);
2002
+ if (tagStatus === "preview")
2003
+ addDiagnostic(
2004
+ diagnostics,
2005
+ source,
2006
+ token.start,
2007
+ `<${token.name}> is an Azure Speech preview feature and may change or require preview access.`,
2008
+ "warning",
2009
+ "azure-preview-tag"
2010
+ );
2011
+ if (tagStatus === "deprecated")
2012
+ addDiagnostic(
2013
+ diagnostics,
2014
+ source,
2015
+ token.start,
2016
+ `<${token.name}> is deprecated by Azure Speech; migrate to a supported alternative.`,
2017
+ "info",
2018
+ "azure-deprecated-tag"
2019
+ );
1879
2020
  if (name === "voice" && !attr(token, "name")?.trim())
1880
2021
  addDiagnostic(diagnostics, source, token.start, '<voice> requires a non-empty "name" attribute.');
1881
2022
  if (name === "break") {
@@ -2050,6 +2191,9 @@ function validateAzureSsml(ssml, options = {}) {
2050
2191
  const maxLength = options.maxLength ?? 1e4;
2051
2192
  if (ssml.length > maxLength)
2052
2193
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
2194
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
2195
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
2196
+ }
2053
2197
  try {
2054
2198
  parseSsml(ssml);
2055
2199
  } catch (error) {
@@ -2059,6 +2203,18 @@ function validateAzureSsml(ssml, options = {}) {
2059
2203
  return diagnostics;
2060
2204
  }
2061
2205
  const tokens = tokenizeElements(ssml);
2206
+ if (options.maxXmlDepth !== void 0) {
2207
+ for (const token of tokens) {
2208
+ if (token.depth > options.maxXmlDepth) {
2209
+ addDiagnostic(
2210
+ diagnostics,
2211
+ ssml,
2212
+ token.start,
2213
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
2214
+ );
2215
+ }
2216
+ }
2217
+ }
2062
2218
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2063
2219
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2064
2220
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -2087,6 +2243,24 @@ function validateAzureSsml(ssml, options = {}) {
2087
2243
  const name = attr(token, "name")?.trim();
2088
2244
  const language = attr(token, "xml:lang")?.trim() || (speak ? attr(speak, "xml:lang")?.trim() : void 0);
2089
2245
  const definition = name ? voiceCatalog.get(name.toLowerCase()) : void 0;
2246
+ if (name && definition?.status === "preview")
2247
+ addDiagnostic(
2248
+ diagnostics,
2249
+ ssml,
2250
+ token.start,
2251
+ `Voice "${name}" is an Azure Speech preview voice and may change or require preview access.`,
2252
+ "warning",
2253
+ "azure-preview-voice"
2254
+ );
2255
+ if (name && definition?.status === "deprecated")
2256
+ addDiagnostic(
2257
+ diagnostics,
2258
+ ssml,
2259
+ token.start,
2260
+ `Voice "${name}" is deprecated by Azure Speech; migrate to a supported voice.`,
2261
+ "info",
2262
+ "azure-deprecated-voice"
2263
+ );
2090
2264
  if (name && !definition && policySeverity)
2091
2265
  addDiagnostic(
2092
2266
  diagnostics,
@@ -2249,7 +2423,8 @@ function closeSpeechResources(speechConfig, synthesizer) {
2249
2423
  } catch {
2250
2424
  }
2251
2425
  }
2252
- async function synthesizeSpeech(ssml, config) {
2426
+ var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
2427
+ async function synthesizeSsml(ssml, config) {
2253
2428
  if (config.signal?.aborted) {
2254
2429
  throw createSpeechSdkError("Speech synthesis was cancelled.");
2255
2430
  }
@@ -2276,6 +2451,22 @@ async function synthesizeSpeech(ssml, config) {
2276
2451
  closeResources();
2277
2452
  reject(createSpeechSdkError(error));
2278
2453
  };
2454
+ const boundaries = [];
2455
+ const visemes = [];
2456
+ const bookmarks = [];
2457
+ synthesizer.wordBoundary = (_sender, event) => {
2458
+ boundaries.push({
2459
+ text: event.text,
2460
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
2461
+ durationMs: ticksToMilliseconds(event.duration)
2462
+ });
2463
+ };
2464
+ synthesizer.visemeReceived = (_sender, event) => {
2465
+ visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
2466
+ };
2467
+ synthesizer.bookmarkReached = (_sender, event) => {
2468
+ bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
2469
+ };
2279
2470
  const cb = (result) => {
2280
2471
  if (settled) return;
2281
2472
  const { reason, errorDetails } = result;
@@ -2287,7 +2478,20 @@ async function synthesizeSpeech(ssml, config) {
2287
2478
  settled = true;
2288
2479
  cleanup();
2289
2480
  closeResources();
2290
- resolve(result.audioData);
2481
+ const eventDurationMs = Math.max(
2482
+ 0,
2483
+ ...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
2484
+ ...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
2485
+ ...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
2486
+ );
2487
+ const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
2488
+ resolve({
2489
+ audioData: result.audioData,
2490
+ durationMs,
2491
+ ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
2492
+ ...visemes.length > 0 ? { visemes } : {},
2493
+ ...bookmarks.length > 0 ? { bookmarks } : {}
2494
+ });
2291
2495
  };
2292
2496
  try {
2293
2497
  if (config.signal) {
@@ -2306,6 +2510,9 @@ async function synthesizeSpeech(ssml, config) {
2306
2510
  }
2307
2511
  });
2308
2512
  }
2513
+ async function synthesizeSpeech(ssml, config) {
2514
+ return (await synthesizeSsml(ssml, config)).audioData;
2515
+ }
2309
2516
 
2310
2517
  // packages/azure-tts-client/src/client.ts
2311
2518
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -2322,8 +2529,92 @@ var AzureTtsClient = class {
2322
2529
  const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
2323
2530
  return synthesizeSpeech(ssml, config);
2324
2531
  }
2532
+ async synthesizeSsml(ssml) {
2533
+ const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
2534
+ const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
2535
+ __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
2536
+ return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
2537
+ }
2325
2538
  };
2326
2539
  _options = new WeakMap();
2540
+
2541
+ // packages/azure-tts-client/src/voiceCatalog.ts
2542
+ var AZURE_VOICE_API_VERSION = "2025-10-01";
2543
+ function stringValue(value) {
2544
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2545
+ }
2546
+ function stringList(value) {
2547
+ if (!Array.isArray(value)) return [];
2548
+ return [...new Set(value.map(stringValue).filter((item) => item !== void 0))];
2549
+ }
2550
+ function normalizeStatus(value) {
2551
+ const status = stringValue(value)?.toLowerCase();
2552
+ if (status === "preview" || status === "deprecated" || status === "ga") return status;
2553
+ return void 0;
2554
+ }
2555
+ function normalizeRegions(region) {
2556
+ const regions = Array.isArray(region) ? region : [region];
2557
+ const result = [...new Set(regions.map((item) => item.trim()).filter(Boolean))];
2558
+ if (result.length === 0) throw new TypeError("At least one Azure Speech region is required.");
2559
+ return result;
2560
+ }
2561
+ async function fetchRegionVoices(region, apiKey) {
2562
+ const endpoint = `https://${encodeURIComponent(region)}.tts.speech.microsoft.com/cognitiveservices/voices/list`;
2563
+ const response = await fetch(endpoint, {
2564
+ headers: {
2565
+ Accept: "application/json",
2566
+ "Ocp-Apim-Subscription-Key": apiKey
2567
+ }
2568
+ });
2569
+ if (!response.ok) {
2570
+ throw new Error(`Azure List Voices API request failed for region "${region}" with HTTP ${response.status}.`);
2571
+ }
2572
+ const payload = await response.json();
2573
+ if (!Array.isArray(payload)) throw new Error(`Azure List Voices API returned an invalid response for "${region}".`);
2574
+ return payload.filter((item) => Boolean(item && typeof item === "object"));
2575
+ }
2576
+ async function fetchAzureVoiceCatalog(options) {
2577
+ if (!options || typeof options.apiKey !== "string" || !options.apiKey.trim())
2578
+ throw new TypeError("An Azure Speech API key is required.");
2579
+ const regions = normalizeRegions(options.region);
2580
+ const payloads = await Promise.all(regions.map((region) => fetchRegionVoices(region, options.apiKey)));
2581
+ const voices = /* @__PURE__ */ new Map();
2582
+ for (let regionIndex = 0; regionIndex < payloads.length; regionIndex += 1) {
2583
+ const region = regions[regionIndex];
2584
+ for (const record of payloads[regionIndex]) {
2585
+ const name = stringValue(record.ShortName) ?? stringValue(record.Name);
2586
+ const locale = stringValue(record.Locale);
2587
+ if (!name || !locale) continue;
2588
+ const key = name.toLowerCase();
2589
+ const existing = voices.get(key);
2590
+ const secondaryLocales = stringList(record.SecondaryLocaleList);
2591
+ const styles = stringList(record.StyleList);
2592
+ const status = normalizeStatus(record.Status);
2593
+ const merged = {
2594
+ name: existing?.name ?? name,
2595
+ locale: existing?.locale ?? locale,
2596
+ regions: [.../* @__PURE__ */ new Set([...existing?.regions ?? [], region])]
2597
+ };
2598
+ const mergedSecondaryLocales = [.../* @__PURE__ */ new Set([...existing?.secondaryLocales ?? [], ...secondaryLocales])];
2599
+ if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
2600
+ const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
2601
+ if (mergedStyles.length > 0) merged.styles = mergedStyles;
2602
+ if (status) merged.status = status;
2603
+ else if (existing?.status) merged.status = existing.status;
2604
+ voices.set(key, merged);
2605
+ }
2606
+ }
2607
+ const sortedVoices = [...voices.values()].sort((first, second) => first.name.localeCompare(second.name));
2608
+ return {
2609
+ voices: sortedVoices,
2610
+ metadata: {
2611
+ voiceCount: sortedVoices.length,
2612
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2613
+ apiVersion: AZURE_VOICE_API_VERSION,
2614
+ regions
2615
+ }
2616
+ };
2617
+ }
2327
2618
  // Annotate the CommonJS export names for ESM import in node:
2328
2619
  0 && (module.exports = {
2329
2620
  AzureTtsClient,
@@ -2334,6 +2625,7 @@ _options = new WeakMap();
2334
2625
  buildSsml,
2335
2626
  extractSsmlText,
2336
2627
  extractSsmlTranslatableText,
2628
+ fetchAzureVoiceCatalog,
2337
2629
  fromPlainTextToSsml,
2338
2630
  getAzureVoiceCatalogMetadata,
2339
2631
  getBuiltInVoiceCatalogMetadata,
@@ -2341,7 +2633,9 @@ _options = new WeakMap();
2341
2633
  mapSsmlTextNodes,
2342
2634
  normalizeAzureLanguage,
2343
2635
  parseSsml,
2636
+ splitSsmlDocument,
2344
2637
  synthesizeSpeech,
2638
+ synthesizeSsml,
2345
2639
  validateAzureSsml,
2346
2640
  validateSsml,
2347
2641
  validateSsmlStructureIntegrity