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/elements.mjs CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  resolveExpressAsStyles,
37
37
  updateEditableText,
38
38
  validateAzureSsml
39
- } from "./chunk-FWWMWI2C.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,11 @@ 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
75
  interface FetchAzureVoiceCatalogOptions {
@@ -69,4 +97,4 @@ interface AzureVoiceCatalog {
69
97
  /** Fetches and deduplicates the current Azure Speech voice catalog for one or more regions. */
70
98
  declare function fetchAzureVoiceCatalog(options: FetchAzureVoiceCatalogOptions): Promise<AzureVoiceCatalog>;
71
99
 
72
- export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type AzureVoiceCatalog, type AzureVoiceCatalogVoice, type FetchAzureVoiceCatalogOptions, type FetchedAzureVoiceCatalogMetadata, type TtsConfig, fetchAzureVoiceCatalog, synthesizeSpeech };
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,11 @@ 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
75
  interface FetchAzureVoiceCatalogOptions {
@@ -69,4 +97,4 @@ interface AzureVoiceCatalog {
69
97
  /** Fetches and deduplicates the current Azure Speech voice catalog for one or more regions. */
70
98
  declare function fetchAzureVoiceCatalog(options: FetchAzureVoiceCatalogOptions): Promise<AzureVoiceCatalog>;
71
99
 
72
- export { AzureTtsClient, type AzureTtsClientOptions, AzureTtsError, type AzureTtsLogger, AzureTtsSdkError, type AzureVoiceCatalog, type AzureVoiceCatalogVoice, type FetchAzureVoiceCatalogOptions, type FetchedAzureVoiceCatalogMetadata, type TtsConfig, fetchAzureVoiceCatalog, synthesizeSpeech };
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
@@ -53,7 +53,9 @@ __export(src_exports, {
53
53
  mapSsmlTextNodes: () => mapSsmlTextNodes,
54
54
  normalizeAzureLanguage: () => normalizeAzureLanguage,
55
55
  parseSsml: () => parseSsml,
56
+ splitSsmlDocument: () => splitSsmlDocument,
56
57
  synthesizeSpeech: () => synthesizeSpeech,
58
+ synthesizeSsml: () => synthesizeSsml,
57
59
  validateAzureSsml: () => validateAzureSsml,
58
60
  validateSsml: () => validateSsml,
59
61
  validateSsmlStructureIntegrity: () => validateSsmlStructureIntegrity
@@ -987,6 +989,104 @@ function buildPartialSsml(textOrOptions, context) {
987
989
  return serializePartialSsml(textOrOptions.text, textOrOptions);
988
990
  }
989
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
+
990
1090
  // packages/ssml-core/src/validation.ts
991
1091
  var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
992
1092
  function validateSsml(xmlString) {
@@ -1681,6 +1781,7 @@ function tokenizeElements(source) {
1681
1781
  attributes,
1682
1782
  childElementIndex,
1683
1783
  end,
1784
+ depth: openElements.length + 1,
1684
1785
  name: tokenName,
1685
1786
  parentName: parent?.name,
1686
1787
  parentVoiceName,
@@ -1871,13 +1972,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
1871
1972
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
1872
1973
  return;
1873
1974
  }
1975
+ if (parsed.username || parsed.password)
1976
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
1874
1977
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
1875
1978
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
1876
1979
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
1877
1980
  try {
1878
- 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;
1879
1985
  } catch {
1880
- return allowedOrigin === parsed.origin;
1986
+ return false;
1881
1987
  }
1882
1988
  }) ?? false;
1883
1989
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -2085,6 +2191,9 @@ function validateAzureSsml(ssml, options = {}) {
2085
2191
  const maxLength = options.maxLength ?? 1e4;
2086
2192
  if (ssml.length > maxLength)
2087
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
+ }
2088
2197
  try {
2089
2198
  parseSsml(ssml);
2090
2199
  } catch (error) {
@@ -2094,6 +2203,18 @@ function validateAzureSsml(ssml, options = {}) {
2094
2203
  return diagnostics;
2095
2204
  }
2096
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
+ }
2097
2218
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
2098
2219
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
2099
2220
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -2302,7 +2423,8 @@ function closeSpeechResources(speechConfig, synthesizer) {
2302
2423
  } catch {
2303
2424
  }
2304
2425
  }
2305
- async function synthesizeSpeech(ssml, config) {
2426
+ var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
2427
+ async function synthesizeSsml(ssml, config) {
2306
2428
  if (config.signal?.aborted) {
2307
2429
  throw createSpeechSdkError("Speech synthesis was cancelled.");
2308
2430
  }
@@ -2329,6 +2451,22 @@ async function synthesizeSpeech(ssml, config) {
2329
2451
  closeResources();
2330
2452
  reject(createSpeechSdkError(error));
2331
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
+ };
2332
2470
  const cb = (result) => {
2333
2471
  if (settled) return;
2334
2472
  const { reason, errorDetails } = result;
@@ -2340,7 +2478,20 @@ async function synthesizeSpeech(ssml, config) {
2340
2478
  settled = true;
2341
2479
  cleanup();
2342
2480
  closeResources();
2343
- 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
+ });
2344
2495
  };
2345
2496
  try {
2346
2497
  if (config.signal) {
@@ -2359,6 +2510,9 @@ async function synthesizeSpeech(ssml, config) {
2359
2510
  }
2360
2511
  });
2361
2512
  }
2513
+ async function synthesizeSpeech(ssml, config) {
2514
+ return (await synthesizeSsml(ssml, config)).audioData;
2515
+ }
2362
2516
 
2363
2517
  // packages/azure-tts-client/src/client.ts
2364
2518
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -2375,6 +2529,12 @@ var AzureTtsClient = class {
2375
2529
  const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
2376
2530
  return synthesizeSpeech(ssml, config);
2377
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
+ }
2378
2538
  };
2379
2539
  _options = new WeakMap();
2380
2540
 
@@ -2473,7 +2633,9 @@ async function fetchAzureVoiceCatalog(options) {
2473
2633
  mapSsmlTextNodes,
2474
2634
  normalizeAzureLanguage,
2475
2635
  parseSsml,
2636
+ splitSsmlDocument,
2476
2637
  synthesizeSpeech,
2638
+ synthesizeSsml,
2477
2639
  validateAzureSsml,
2478
2640
  validateSsml,
2479
2641
  validateSsmlStructureIntegrity