ssml-builder-js 2.14.0 → 2.15.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/index.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  fromPlainTextToSsml,
9
9
  getAzureVoiceCatalogMetadata,
10
10
  getBuiltInVoiceCatalogMetadata,
11
+ getSsmlSourceMap,
11
12
  isValidAzureAudioDuration,
12
13
  mapSsmlTextNodes,
13
14
  normalizeAzureLanguage,
@@ -16,10 +17,11 @@ import {
16
17
  validateAzureSsml,
17
18
  validateSsml,
18
19
  validateSsmlStructureIntegrity
19
- } from "./chunk-AQ55MOPU.mjs";
20
+ } from "./chunk-FXUM45ZY.mjs";
20
21
  import {
22
+ getSsmlSourceMap as getSsmlSourceMap2,
21
23
  validateAzureSsml as validateAzureSsml2
22
- } from "./chunk-QFIBPCO4.mjs";
24
+ } from "./chunk-WXFLUCLR.mjs";
23
25
  import {
24
26
  __privateAdd,
25
27
  __privateGet,
@@ -30,6 +32,7 @@ import {
30
32
  var AzureTtsError = class extends Error {
31
33
  constructor(status, statusText, responseBody, requestId) {
32
34
  super(`Azure TTS request failed: ${status} ${statusText}`);
35
+ this.kind = "azure-api-error";
33
36
  this.name = "AzureTtsError";
34
37
  this.status = status;
35
38
  this.statusText = statusText;
@@ -45,13 +48,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
45
48
  this.errorDetails = errorDetails;
46
49
  }
47
50
  };
51
+ var SynthesisCancelledError = class extends Error {
52
+ constructor(message = "Speech synthesis was cancelled.") {
53
+ super(message);
54
+ this.kind = "cancelled";
55
+ this.name = "SynthesisCancelledError";
56
+ }
57
+ };
58
+ var SynthesisTimeoutError = class extends Error {
59
+ constructor(message) {
60
+ super(message);
61
+ this.kind = "timeout";
62
+ this.name = "SynthesisTimeoutError";
63
+ }
64
+ };
65
+ var MergeError = class extends Error {
66
+ constructor(message, cause) {
67
+ super(message);
68
+ this.kind = "merge-error";
69
+ this.name = "MergeError";
70
+ this.cause = cause;
71
+ }
72
+ };
48
73
  var UnsupportedMergeFormatError = class extends Error {
49
74
  constructor(format) {
50
75
  super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
76
+ this.kind = "unsupported-format-error";
51
77
  this.name = "UnsupportedMergeFormatError";
52
78
  this.format = format;
53
79
  }
54
80
  };
81
+ function toSynthesisError(error) {
82
+ if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
83
+ return error;
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
86
+ if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
87
+ return createSpeechSdkError(error);
88
+ }
55
89
  function createSpeechSdkError(error) {
56
90
  const message = error instanceof Error ? error.message : String(error);
57
91
  return new AzureTtsSdkError(message);
@@ -60,9 +94,6 @@ function createSpeechSdkError(error) {
60
94
  // packages/azure-tts-client/src/synthesis.ts
61
95
  import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
62
96
 
63
- // packages/azure-tts-client/src/speechConfig.ts
64
- import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
65
-
66
97
  // packages/azure-tts-client/src/outputFormats.ts
67
98
  import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
68
99
  var DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
@@ -107,6 +138,14 @@ var OUTPUT_FORMATS = {
107
138
  "amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
108
139
  "g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
109
140
  };
141
+ function resolveMimeType(outputFormat) {
142
+ if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
143
+ if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
144
+ if (/ogg/i.test(outputFormat)) return "audio/ogg";
145
+ if (/webm/i.test(outputFormat)) return "audio/webm";
146
+ if (/raw/i.test(outputFormat)) return "audio/L16";
147
+ return "application/octet-stream";
148
+ }
110
149
  function resolveOutputFormat(outputFormat) {
111
150
  const resolvedFormat = OUTPUT_FORMATS[outputFormat];
112
151
  if (resolvedFormat === void 0) {
@@ -116,6 +155,7 @@ function resolveOutputFormat(outputFormat) {
116
155
  }
117
156
 
118
157
  // packages/azure-tts-client/src/speechConfig.ts
158
+ import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
119
159
  function resolveEndpoint(config) {
120
160
  const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
121
161
  return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
@@ -249,28 +289,35 @@ function resolveMergeAudioFormat(format) {
249
289
  function canMergeAudioFormat(format) {
250
290
  return resolveMergeAudioFormat(format) !== void 0;
251
291
  }
252
- function mergeAudioBuffers(buffers, format) {
253
- if (isWavFormat(format)) return mergeWavBuffers(buffers);
254
- if (isMp3Format(format)) {
255
- const parts = buffers.map(stripMp3Tags);
256
- const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
257
- let offset = 0;
258
- for (const part of parts) {
259
- output.set(part, offset);
260
- offset += part.byteLength;
292
+ function mergeAudioBuffers(buffers, options) {
293
+ const format = typeof options === "string" ? options : options?.format;
294
+ if (!format) throw new UnsupportedMergeFormatError("");
295
+ try {
296
+ if (isWavFormat(format)) return mergeWavBuffers(buffers);
297
+ if (isMp3Format(format)) {
298
+ const parts = buffers.map(stripMp3Tags);
299
+ const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
300
+ let offset = 0;
301
+ for (const part of parts) {
302
+ output.set(part, offset);
303
+ offset += part.byteLength;
304
+ }
305
+ return output.buffer;
261
306
  }
262
- return output.buffer;
263
- }
264
- if (isRawFormat(format)) {
265
- const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
266
- let offset = 0;
267
- for (const buffer of buffers) {
268
- output.set(new Uint8Array(buffer), offset);
269
- offset += buffer.byteLength;
307
+ if (isRawFormat(format)) {
308
+ const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
309
+ let offset = 0;
310
+ for (const buffer of buffers) {
311
+ output.set(new Uint8Array(buffer), offset);
312
+ offset += buffer.byteLength;
313
+ }
314
+ return output.buffer;
270
315
  }
271
- return output.buffer;
316
+ throw new UnsupportedMergeFormatError(format);
317
+ } catch (error) {
318
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
319
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
272
320
  }
273
- throw new UnsupportedMergeFormatError(format);
274
321
  }
275
322
  function closeSpeechResources(speechConfig, synthesizer) {
276
323
  try {
@@ -285,7 +332,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
285
332
  var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
286
333
  async function synthesizeSsml(ssml, config) {
287
334
  if (config.signal?.aborted) {
288
- throw createSpeechSdkError("Speech synthesis was cancelled.");
335
+ throw new SynthesisCancelledError();
289
336
  }
290
337
  const speechConfig = createSpeechConfig(config);
291
338
  const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
@@ -308,23 +355,94 @@ async function synthesizeSsml(ssml, config) {
308
355
  settled = true;
309
356
  cleanup();
310
357
  closeResources();
311
- reject(createSpeechSdkError(error));
358
+ reject(toSynthesisError(error));
312
359
  };
313
360
  const boundaries = [];
314
361
  const visemes = [];
315
362
  const bookmarks = [];
363
+ let sourceEventCursor = 0;
364
+ let generatedSourceMap;
365
+ if (!config.sourceTextSegments && !config.sourceMarkers) {
366
+ try {
367
+ generatedSourceMap = getSsmlSourceMap2(ssml);
368
+ } catch {
369
+ generatedSourceMap = void 0;
370
+ }
371
+ }
372
+ const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
373
+ const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
374
+ ...segment,
375
+ range: {
376
+ start: segment.range.start + sourceBaseOffset,
377
+ end: segment.range.end + sourceBaseOffset
378
+ },
379
+ sourceNodePath: [...segment.sourceNodePath]
380
+ })) ?? [];
381
+ const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
382
+ ...marker,
383
+ originalTextRange: {
384
+ start: marker.originalTextRange.start + sourceBaseOffset,
385
+ end: marker.originalTextRange.end + sourceBaseOffset
386
+ },
387
+ sourceNodePath: [...marker.sourceNodePath]
388
+ })) ?? [];
389
+ const sourceText = sourceSegments.map((segment) => segment.text).join("");
390
+ const mapSourceEvent = (text, offsetHint, markerName) => {
391
+ const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
392
+ if (marker) {
393
+ return {
394
+ originalTextRange: { ...marker.originalTextRange },
395
+ sourceNodePath: [...marker.sourceNodePath],
396
+ textRange: { ...marker.originalTextRange }
397
+ };
398
+ }
399
+ if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
400
+ const value = text ?? "";
401
+ let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
402
+ if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
403
+ localStart = -1;
404
+ if (localStart < 0 || localStart > sourceText.length) {
405
+ localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
406
+ if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
407
+ }
408
+ localStart = Math.max(0, localStart);
409
+ const localEnd = Math.min(sourceText.length, localStart + value.length);
410
+ sourceEventCursor = Math.max(sourceEventCursor, localEnd);
411
+ const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
412
+ const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
413
+ const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
414
+ return {
415
+ originalTextRange: { ...fallbackRange },
416
+ textRange: { ...fallbackRange },
417
+ ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
418
+ };
419
+ };
316
420
  synthesizer.wordBoundary = (_sender, event) => {
317
421
  boundaries.push({
318
422
  text: event.text,
319
423
  audioOffsetMs: ticksToMilliseconds(event.audioOffset),
320
- durationMs: ticksToMilliseconds(event.duration)
424
+ durationMs: ticksToMilliseconds(event.duration),
425
+ ...mapSourceEvent(
426
+ event.text,
427
+ event.textOffset
428
+ )
321
429
  });
322
430
  };
323
431
  synthesizer.visemeReceived = (_sender, event) => {
324
- visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
432
+ const eventWithOffset = event;
433
+ visemes.push({
434
+ visemeId: event.visemeId,
435
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
436
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset)
437
+ });
325
438
  };
326
439
  synthesizer.bookmarkReached = (_sender, event) => {
327
- bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
440
+ const eventWithOffset = event;
441
+ bookmarks.push({
442
+ name: event.text,
443
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
444
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
445
+ });
328
446
  };
329
447
  const cb = (result) => {
330
448
  if (settled) return;
@@ -347,8 +465,8 @@ async function synthesizeSsml(ssml, config) {
347
465
  const requestId = result.resultId;
348
466
  const addSourceMetadata = (event) => ({
349
467
  ...event,
350
- ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
351
- ...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
468
+ ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
469
+ ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
352
470
  ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
353
471
  ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
354
472
  ...requestId ? { requestId } : {}
@@ -368,12 +486,12 @@ async function synthesizeSsml(ssml, config) {
368
486
  };
369
487
  try {
370
488
  if (config.signal) {
371
- abortHandler = () => rejectWithError("Speech synthesis was cancelled.");
489
+ abortHandler = () => rejectWithError(new SynthesisCancelledError());
372
490
  config.signal.addEventListener("abort", abortHandler, { once: true });
373
491
  }
374
492
  if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
375
493
  timeout = setTimeout(
376
- () => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
494
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
377
495
  config.timeoutMs
378
496
  );
379
497
  }
@@ -415,7 +533,9 @@ async function synthesizeSsmlChunks(chunks, config) {
415
533
  const result = await synthesizeSsml(input.ssml, {
416
534
  ...config,
417
535
  ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
418
- ...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
536
+ ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
537
+ ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
538
+ ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
419
539
  chunkIndex: index,
420
540
  onProgress: void 0
421
541
  });
@@ -443,27 +563,16 @@ async function synthesizeSsmlChunks(chunks, config) {
443
563
  throw error;
444
564
  }
445
565
  }
446
- return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
447
- }
448
- function mergeSynthesisResults(results, format) {
449
- const audioData = format ? new Uint8Array(
450
- mergeAudioBuffers(
451
- results.map((result) => result.audioData),
452
- format
453
- )
454
- ) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
455
- if (!format) {
456
- let offset = 0;
457
- for (const result of results) {
458
- audioData.set(new Uint8Array(result.audioData), offset);
459
- offset += result.audioData.byteLength;
460
- }
461
- }
566
+ return mergeSynthesisResults(results, {
567
+ format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
568
+ });
569
+ }
570
+ function createMergedResult(results, audioData, format) {
462
571
  const boundaries = [];
463
572
  const visemes = [];
464
573
  const bookmarks = [];
465
574
  let durationOffset = 0;
466
- for (const result of results) {
575
+ for (const [resultIndex, result] of results.entries()) {
467
576
  const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
468
577
  for (const boundary of chunkBoundaries) {
469
578
  const textRange = boundary.textRange ?? result.textRange;
@@ -473,7 +582,7 @@ function mergeSynthesisResults(results, format) {
473
582
  ...boundary,
474
583
  audioOffsetMs: boundary.audioOffsetMs + durationOffset,
475
584
  chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
476
- ...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
585
+ ...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
477
586
  ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
478
587
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
479
588
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -488,7 +597,7 @@ function mergeSynthesisResults(results, format) {
488
597
  ...viseme,
489
598
  audioOffsetMs: viseme.audioOffsetMs + durationOffset,
490
599
  chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
491
- ...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
600
+ ...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
492
601
  ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
493
602
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
494
603
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -503,7 +612,7 @@ function mergeSynthesisResults(results, format) {
503
612
  ...bookmark,
504
613
  audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
505
614
  chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
506
- ...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
615
+ ...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
507
616
  ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
508
617
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
509
618
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -513,8 +622,9 @@ function mergeSynthesisResults(results, format) {
513
622
  durationOffset += Math.max(0, result.durationMs);
514
623
  }
515
624
  return {
516
- audioData: audioData.buffer,
625
+ audioData,
517
626
  durationMs: durationOffset,
627
+ mimeType: resolveMimeType(format),
518
628
  ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
519
629
  ...visemes.length > 0 ? { visemes } : {},
520
630
  ...bookmarks.length > 0 ? { bookmarks } : {},
@@ -522,6 +632,27 @@ function mergeSynthesisResults(results, format) {
522
632
  ...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
523
633
  };
524
634
  }
635
+ function mergeSynthesisResults(results, options) {
636
+ const resolvedOptions = typeof options === "string" ? { format: options } : options;
637
+ const format = resolvedOptions?.format;
638
+ if (!format) throw new UnsupportedMergeFormatError("");
639
+ const buffers = results.map((result) => result.audioData);
640
+ if (resolvedOptions.customMerger) {
641
+ return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
642
+ if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
643
+ return createMergedResult(results, merged, format);
644
+ }).catch((error) => {
645
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
646
+ throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
647
+ });
648
+ }
649
+ try {
650
+ return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
651
+ } catch (error) {
652
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
653
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
654
+ }
655
+ }
525
656
  async function synthesizeSpeech(ssml, config) {
526
657
  return (await synthesizeSsml(ssml, config)).audioData;
527
658
  }
@@ -530,37 +661,48 @@ async function synthesizeSpeech(ssml, config) {
530
661
  var ChunkValidationError = class extends Error {
531
662
  constructor(chunkIndex, diagnostics) {
532
663
  super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
533
- this.kind = "chunk-validation";
664
+ this.kind = "validation-error";
534
665
  this.name = "ChunkValidationError";
535
666
  this.chunkIndex = chunkIndex;
536
667
  this.diagnostics = diagnostics;
537
668
  }
538
669
  };
670
+ function failure(error) {
671
+ return { ok: false, success: false, status: error.kind, error };
672
+ }
539
673
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
540
- const validationOptions = options.validation ?? options;
674
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
541
675
  const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
676
+ if (options.signal?.aborted) {
677
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
678
+ return failure(error);
679
+ }
542
680
  const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
543
681
  if (errors.length > 0) {
544
- return {
545
- ok: false,
546
- success: false,
547
- status: "validation-error",
548
- error: {
549
- kind: "validation",
550
- message: "SSML validation failed; the Azure Speech API was not called.",
551
- diagnostics: errors
552
- }
553
- };
682
+ return failure({
683
+ kind: "validation-error",
684
+ message: "SSML validation failed; the Azure Speech API was not called.",
685
+ diagnostics: errors
686
+ });
554
687
  }
555
688
  try {
556
- return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
689
+ return {
690
+ ok: true,
691
+ success: true,
692
+ status: "success",
693
+ value: await client.synthesizeSsml(ssml, { signal: options.signal })
694
+ };
557
695
  } catch (error) {
558
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
559
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
696
+ const synthesisError = toSynthesisError(error);
697
+ return failure(synthesisError);
560
698
  }
561
699
  }
562
700
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
563
- const validationOptions = options.validation ?? options;
701
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
702
+ if (options.signal?.aborted) {
703
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
704
+ return failure(error);
705
+ }
564
706
  const pending = (index, status, error) => {
565
707
  options.onProgress?.({
566
708
  currentChunk: status === "success" ? index + 1 : index,
@@ -579,7 +721,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
579
721
  const validations = await Promise.all(
580
722
  chunks.map(async (chunk) => {
581
723
  const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
582
- const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
724
+ const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
725
+ const diagnostics = await Promise.resolve(
726
+ validateAzureSsml2(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
727
+ );
583
728
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
584
729
  })
585
730
  );
@@ -587,31 +732,78 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
587
732
  if (firstInvalidIndex >= 0) {
588
733
  const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
589
734
  pending(firstInvalidIndex, "failed", error);
590
- return { ok: false, success: false, status: "validation-error", error };
735
+ return failure(error);
591
736
  }
592
737
  try {
593
738
  if (client.synthesizeChunks) {
594
- const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
739
+ const normalizedChunks = chunks.map((chunk) => {
740
+ if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
741
+ return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
742
+ });
743
+ const value = await client.synthesizeChunks(normalizedChunks, {
744
+ onProgress: options.onProgress,
745
+ outputFormat: options.outputFormat,
746
+ signal: options.signal,
747
+ timeoutMs: options.timeoutMs,
748
+ sourceNodePath: options.sourceNodePath
749
+ });
595
750
  return { ok: true, success: true, status: "success", value };
596
751
  }
597
752
  const results = [];
598
753
  for (const [index, chunk] of chunks.entries()) {
599
754
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
600
755
  const sourceNodePath = input.sourceNodePath;
756
+ const originalTextRange = input.originalTextRange;
601
757
  pending(index, "synthesizing");
602
758
  const startedAt = Date.now();
603
759
  try {
604
- const result = await client.synthesizeSsml(input.ssml);
760
+ const result = await client.synthesizeSsml(input.ssml, {
761
+ outputFormat: options.outputFormat,
762
+ signal: options.signal,
763
+ timeoutMs: options.timeoutMs,
764
+ sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
765
+ });
605
766
  results.push({
606
767
  ...result,
607
768
  ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
608
769
  ...sourceNodePath ? {
609
770
  boundaries: result.boundaries?.map((event) => ({
610
771
  ...event,
611
- sourceNodePath: [...sourceNodePath]
772
+ sourceNodePath: [...sourceNodePath],
773
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
774
+ })),
775
+ visemes: result.visemes?.map((event) => ({
776
+ ...event,
777
+ sourceNodePath: [...sourceNodePath],
778
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
779
+ })),
780
+ bookmarks: result.bookmarks?.map((event) => ({
781
+ ...event,
782
+ sourceNodePath: [...sourceNodePath],
783
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
784
+ }))
785
+ } : {},
786
+ ...originalTextRange ? {
787
+ boundaries: result.boundaries?.map((event) => ({
788
+ ...event,
789
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
612
790
  })),
613
- visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
614
- bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
791
+ wordBoundary: result.wordBoundary?.map((event) => ({
792
+ ...event,
793
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
794
+ })),
795
+ wordBoundaries: result.wordBoundaries?.map((event) => ({
796
+ ...event,
797
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
798
+ })),
799
+ visemes: result.visemes?.map((event) => ({
800
+ ...event,
801
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
802
+ })),
803
+ bookmarks: result.bookmarks?.map((event) => ({
804
+ ...event,
805
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
806
+ }))
615
807
  } : {}
616
808
  });
617
809
  options.onProgress?.({
@@ -641,13 +833,23 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
641
833
  ok: true,
642
834
  success: true,
643
835
  status: "success",
644
- value: mergeSynthesisResults(results, options.outputFormat)
836
+ value: mergeSynthesisResults(results, {
837
+ format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
838
+ })
645
839
  };
646
840
  } catch (error) {
647
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
648
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
841
+ const synthesisError = toSynthesisError(error);
842
+ return failure(synthesisError);
649
843
  }
650
844
  }
845
+ function withValidationSignal(options, signal) {
846
+ if (!signal) return options;
847
+ return {
848
+ ...options,
849
+ urlValidatorSignal: signal,
850
+ urlValidation: { ...options.urlValidation ?? {}, signal }
851
+ };
852
+ }
651
853
 
652
854
  // packages/azure-tts-client/src/client.ts
653
855
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -664,11 +866,21 @@ var AzureTtsClient = class {
664
866
  const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
665
867
  return synthesizeSpeech(ssml, config);
666
868
  }
667
- async synthesizeSsml(ssml) {
869
+ async synthesizeSsml(ssml, options = {}) {
668
870
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
669
871
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
670
872
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
671
- return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
873
+ return synthesizeSsml(ssml, {
874
+ endpoint,
875
+ region,
876
+ subscriptionKey,
877
+ outputFormat: options.outputFormat ?? outputFormat,
878
+ signal: options.signal ?? signal,
879
+ timeoutMs: options.timeoutMs ?? timeoutMs,
880
+ sourceNodePath: options.sourceNodePath,
881
+ sourceTextSegments: options.sourceTextSegments,
882
+ sourceMarkers: options.sourceMarkers
883
+ });
672
884
  }
673
885
  async synthesizeChunks(chunks, options = {}) {
674
886
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
@@ -677,9 +889,10 @@ var AzureTtsClient = class {
677
889
  endpoint,
678
890
  region,
679
891
  subscriptionKey,
680
- outputFormat,
681
- signal,
682
- timeoutMs,
892
+ outputFormat: options.outputFormat ?? outputFormat,
893
+ signal: options.signal ?? signal,
894
+ timeoutMs: options.timeoutMs ?? timeoutMs,
895
+ sourceNodePath: options.sourceNodePath,
683
896
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
684
897
  });
685
898
  }
@@ -690,6 +903,8 @@ var AzureTtsClient = class {
690
903
  return synthesizeSsmlChunksSafe(this, chunks, {
691
904
  ...options,
692
905
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
906
+ signal: options.signal ?? __privateGet(this, _options).signal,
907
+ timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
693
908
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
694
909
  });
695
910
  }
@@ -790,6 +1005,10 @@ export {
790
1005
  AzureTtsError,
791
1006
  AzureTtsSdkError,
792
1007
  ChunkValidationError,
1008
+ DEFAULT_OUTPUT_FORMAT,
1009
+ MergeError,
1010
+ SynthesisCancelledError,
1011
+ SynthesisTimeoutError,
793
1012
  UnsupportedMergeFormatError,
794
1013
  areAzureLanguagesEquivalent,
795
1014
  buildPartialSsml,
@@ -802,6 +1021,7 @@ export {
802
1021
  fromPlainTextToSsml,
803
1022
  getAzureVoiceCatalogMetadata,
804
1023
  getBuiltInVoiceCatalogMetadata,
1024
+ getSsmlSourceMap,
805
1025
  isValidAzureAudioDuration,
806
1026
  mapSsmlTextNodes,
807
1027
  mergeAudioBuffers,
@@ -809,6 +1029,7 @@ export {
809
1029
  normalizeAzureLanguage,
810
1030
  parseSsml,
811
1031
  resolveMergeAudioFormat,
1032
+ resolveMimeType,
812
1033
  splitSsmlDocument,
813
1034
  synthesizeSpeech,
814
1035
  synthesizeSsml,