@ssml-builder-js/azure-tts-client 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.js CHANGED
@@ -41,12 +41,17 @@ __export(index_exports, {
41
41
  AzureTtsError: () => AzureTtsError,
42
42
  AzureTtsSdkError: () => AzureTtsSdkError,
43
43
  ChunkValidationError: () => ChunkValidationError,
44
+ DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
45
+ MergeError: () => MergeError,
46
+ SynthesisCancelledError: () => SynthesisCancelledError,
47
+ SynthesisTimeoutError: () => SynthesisTimeoutError,
44
48
  UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
45
49
  canMergeAudioFormat: () => canMergeAudioFormat,
46
50
  fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
47
51
  mergeAudioBuffers: () => mergeAudioBuffers,
48
52
  mergeSynthesisResults: () => mergeSynthesisResults,
49
53
  resolveMergeAudioFormat: () => resolveMergeAudioFormat,
54
+ resolveMimeType: () => resolveMimeType,
50
55
  synthesizeSpeech: () => synthesizeSpeech,
51
56
  synthesizeSsml: () => synthesizeSsml,
52
57
  synthesizeSsmlChunks: () => synthesizeSsmlChunks,
@@ -59,6 +64,7 @@ module.exports = __toCommonJS(index_exports);
59
64
  var AzureTtsError = class extends Error {
60
65
  constructor(status, statusText, responseBody, requestId) {
61
66
  super(`Azure TTS request failed: ${status} ${statusText}`);
67
+ this.kind = "azure-api-error";
62
68
  this.name = "AzureTtsError";
63
69
  this.status = status;
64
70
  this.statusText = statusText;
@@ -74,13 +80,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
74
80
  this.errorDetails = errorDetails;
75
81
  }
76
82
  };
83
+ var SynthesisCancelledError = class extends Error {
84
+ constructor(message = "Speech synthesis was cancelled.") {
85
+ super(message);
86
+ this.kind = "cancelled";
87
+ this.name = "SynthesisCancelledError";
88
+ }
89
+ };
90
+ var SynthesisTimeoutError = class extends Error {
91
+ constructor(message) {
92
+ super(message);
93
+ this.kind = "timeout";
94
+ this.name = "SynthesisTimeoutError";
95
+ }
96
+ };
97
+ var MergeError = class extends Error {
98
+ constructor(message, cause) {
99
+ super(message);
100
+ this.kind = "merge-error";
101
+ this.name = "MergeError";
102
+ this.cause = cause;
103
+ }
104
+ };
77
105
  var UnsupportedMergeFormatError = class extends Error {
78
106
  constructor(format) {
79
107
  super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
108
+ this.kind = "unsupported-format-error";
80
109
  this.name = "UnsupportedMergeFormatError";
81
110
  this.format = format;
82
111
  }
83
112
  };
113
+ function toSynthesisError(error) {
114
+ if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
115
+ return error;
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
118
+ if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
119
+ return createSpeechSdkError(error);
120
+ }
84
121
  function createSpeechSdkError(error) {
85
122
  const message = error instanceof Error ? error.message : String(error);
86
123
  return new AzureTtsSdkError(message);
@@ -88,9 +125,7 @@ function createSpeechSdkError(error) {
88
125
 
89
126
  // src/synthesis.ts
90
127
  var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
91
-
92
- // src/speechConfig.ts
93
- var import_microsoft_cognitiveservices_speech_sdk = require("microsoft-cognitiveservices-speech-sdk");
128
+ var import_ssml_core = require("@ssml-builder-js/ssml-core");
94
129
 
95
130
  // src/outputFormats.ts
96
131
  var SpeechSDK = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
@@ -136,6 +171,14 @@ var OUTPUT_FORMATS = {
136
171
  "amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
137
172
  "g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
138
173
  };
174
+ function resolveMimeType(outputFormat) {
175
+ if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
176
+ if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
177
+ if (/ogg/i.test(outputFormat)) return "audio/ogg";
178
+ if (/webm/i.test(outputFormat)) return "audio/webm";
179
+ if (/raw/i.test(outputFormat)) return "audio/L16";
180
+ return "application/octet-stream";
181
+ }
139
182
  function resolveOutputFormat(outputFormat) {
140
183
  const resolvedFormat = OUTPUT_FORMATS[outputFormat];
141
184
  if (resolvedFormat === void 0) {
@@ -145,6 +188,7 @@ function resolveOutputFormat(outputFormat) {
145
188
  }
146
189
 
147
190
  // src/speechConfig.ts
191
+ var import_microsoft_cognitiveservices_speech_sdk = require("microsoft-cognitiveservices-speech-sdk");
148
192
  function resolveEndpoint(config) {
149
193
  const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
150
194
  return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
@@ -278,28 +322,35 @@ function resolveMergeAudioFormat(format) {
278
322
  function canMergeAudioFormat(format) {
279
323
  return resolveMergeAudioFormat(format) !== void 0;
280
324
  }
281
- function mergeAudioBuffers(buffers, format) {
282
- if (isWavFormat(format)) return mergeWavBuffers(buffers);
283
- if (isMp3Format(format)) {
284
- const parts = buffers.map(stripMp3Tags);
285
- const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
286
- let offset = 0;
287
- for (const part of parts) {
288
- output.set(part, offset);
289
- offset += part.byteLength;
325
+ function mergeAudioBuffers(buffers, options) {
326
+ const format = typeof options === "string" ? options : options?.format;
327
+ if (!format) throw new UnsupportedMergeFormatError("");
328
+ try {
329
+ if (isWavFormat(format)) return mergeWavBuffers(buffers);
330
+ if (isMp3Format(format)) {
331
+ const parts = buffers.map(stripMp3Tags);
332
+ const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
333
+ let offset = 0;
334
+ for (const part of parts) {
335
+ output.set(part, offset);
336
+ offset += part.byteLength;
337
+ }
338
+ return output.buffer;
290
339
  }
291
- return output.buffer;
292
- }
293
- if (isRawFormat(format)) {
294
- const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
295
- let offset = 0;
296
- for (const buffer of buffers) {
297
- output.set(new Uint8Array(buffer), offset);
298
- offset += buffer.byteLength;
340
+ if (isRawFormat(format)) {
341
+ const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
342
+ let offset = 0;
343
+ for (const buffer of buffers) {
344
+ output.set(new Uint8Array(buffer), offset);
345
+ offset += buffer.byteLength;
346
+ }
347
+ return output.buffer;
299
348
  }
300
- return output.buffer;
349
+ throw new UnsupportedMergeFormatError(format);
350
+ } catch (error) {
351
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
352
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
301
353
  }
302
- throw new UnsupportedMergeFormatError(format);
303
354
  }
304
355
  function closeSpeechResources(speechConfig, synthesizer) {
305
356
  try {
@@ -314,7 +365,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
314
365
  var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
315
366
  async function synthesizeSsml(ssml, config) {
316
367
  if (config.signal?.aborted) {
317
- throw createSpeechSdkError("Speech synthesis was cancelled.");
368
+ throw new SynthesisCancelledError();
318
369
  }
319
370
  const speechConfig = createSpeechConfig(config);
320
371
  const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
@@ -337,23 +388,94 @@ async function synthesizeSsml(ssml, config) {
337
388
  settled = true;
338
389
  cleanup();
339
390
  closeResources();
340
- reject(createSpeechSdkError(error));
391
+ reject(toSynthesisError(error));
341
392
  };
342
393
  const boundaries = [];
343
394
  const visemes = [];
344
395
  const bookmarks = [];
396
+ let sourceEventCursor = 0;
397
+ let generatedSourceMap;
398
+ if (!config.sourceTextSegments && !config.sourceMarkers) {
399
+ try {
400
+ generatedSourceMap = (0, import_ssml_core.getSsmlSourceMap)(ssml);
401
+ } catch {
402
+ generatedSourceMap = void 0;
403
+ }
404
+ }
405
+ const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
406
+ const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
407
+ ...segment,
408
+ range: {
409
+ start: segment.range.start + sourceBaseOffset,
410
+ end: segment.range.end + sourceBaseOffset
411
+ },
412
+ sourceNodePath: [...segment.sourceNodePath]
413
+ })) ?? [];
414
+ const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
415
+ ...marker,
416
+ originalTextRange: {
417
+ start: marker.originalTextRange.start + sourceBaseOffset,
418
+ end: marker.originalTextRange.end + sourceBaseOffset
419
+ },
420
+ sourceNodePath: [...marker.sourceNodePath]
421
+ })) ?? [];
422
+ const sourceText = sourceSegments.map((segment) => segment.text).join("");
423
+ const mapSourceEvent = (text, offsetHint, markerName) => {
424
+ const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
425
+ if (marker) {
426
+ return {
427
+ originalTextRange: { ...marker.originalTextRange },
428
+ sourceNodePath: [...marker.sourceNodePath],
429
+ textRange: { ...marker.originalTextRange }
430
+ };
431
+ }
432
+ if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
433
+ const value = text ?? "";
434
+ let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
435
+ if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
436
+ localStart = -1;
437
+ if (localStart < 0 || localStart > sourceText.length) {
438
+ localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
439
+ if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
440
+ }
441
+ localStart = Math.max(0, localStart);
442
+ const localEnd = Math.min(sourceText.length, localStart + value.length);
443
+ sourceEventCursor = Math.max(sourceEventCursor, localEnd);
444
+ const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
445
+ const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
446
+ 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);
447
+ return {
448
+ originalTextRange: { ...fallbackRange },
449
+ textRange: { ...fallbackRange },
450
+ ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
451
+ };
452
+ };
345
453
  synthesizer.wordBoundary = (_sender, event) => {
346
454
  boundaries.push({
347
455
  text: event.text,
348
456
  audioOffsetMs: ticksToMilliseconds(event.audioOffset),
349
- durationMs: ticksToMilliseconds(event.duration)
457
+ durationMs: ticksToMilliseconds(event.duration),
458
+ ...mapSourceEvent(
459
+ event.text,
460
+ event.textOffset
461
+ )
350
462
  });
351
463
  };
352
464
  synthesizer.visemeReceived = (_sender, event) => {
353
- visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
465
+ const eventWithOffset = event;
466
+ visemes.push({
467
+ visemeId: event.visemeId,
468
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
469
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset)
470
+ });
354
471
  };
355
472
  synthesizer.bookmarkReached = (_sender, event) => {
356
- bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
473
+ const eventWithOffset = event;
474
+ bookmarks.push({
475
+ name: event.text,
476
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
477
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
478
+ });
357
479
  };
358
480
  const cb = (result) => {
359
481
  if (settled) return;
@@ -376,8 +498,8 @@ async function synthesizeSsml(ssml, config) {
376
498
  const requestId = result.resultId;
377
499
  const addSourceMetadata = (event) => ({
378
500
  ...event,
379
- ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
380
- ...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
501
+ ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
502
+ ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
381
503
  ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
382
504
  ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
383
505
  ...requestId ? { requestId } : {}
@@ -397,12 +519,12 @@ async function synthesizeSsml(ssml, config) {
397
519
  };
398
520
  try {
399
521
  if (config.signal) {
400
- abortHandler = () => rejectWithError("Speech synthesis was cancelled.");
522
+ abortHandler = () => rejectWithError(new SynthesisCancelledError());
401
523
  config.signal.addEventListener("abort", abortHandler, { once: true });
402
524
  }
403
525
  if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
404
526
  timeout = setTimeout(
405
- () => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
527
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
406
528
  config.timeoutMs
407
529
  );
408
530
  }
@@ -444,7 +566,9 @@ async function synthesizeSsmlChunks(chunks, config) {
444
566
  const result = await synthesizeSsml(input.ssml, {
445
567
  ...config,
446
568
  ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
447
- ...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
569
+ ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
570
+ ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
571
+ ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
448
572
  chunkIndex: index,
449
573
  onProgress: void 0
450
574
  });
@@ -472,27 +596,16 @@ async function synthesizeSsmlChunks(chunks, config) {
472
596
  throw error;
473
597
  }
474
598
  }
475
- return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
476
- }
477
- function mergeSynthesisResults(results, format) {
478
- const audioData = format ? new Uint8Array(
479
- mergeAudioBuffers(
480
- results.map((result) => result.audioData),
481
- format
482
- )
483
- ) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
484
- if (!format) {
485
- let offset = 0;
486
- for (const result of results) {
487
- audioData.set(new Uint8Array(result.audioData), offset);
488
- offset += result.audioData.byteLength;
489
- }
490
- }
599
+ return mergeSynthesisResults(results, {
600
+ format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
601
+ });
602
+ }
603
+ function createMergedResult(results, audioData, format) {
491
604
  const boundaries = [];
492
605
  const visemes = [];
493
606
  const bookmarks = [];
494
607
  let durationOffset = 0;
495
- for (const result of results) {
608
+ for (const [resultIndex, result] of results.entries()) {
496
609
  const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
497
610
  for (const boundary of chunkBoundaries) {
498
611
  const textRange = boundary.textRange ?? result.textRange;
@@ -502,7 +615,7 @@ function mergeSynthesisResults(results, format) {
502
615
  ...boundary,
503
616
  audioOffsetMs: boundary.audioOffsetMs + durationOffset,
504
617
  chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
505
- ...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
618
+ ...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
506
619
  ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
507
620
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
508
621
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -517,7 +630,7 @@ function mergeSynthesisResults(results, format) {
517
630
  ...viseme,
518
631
  audioOffsetMs: viseme.audioOffsetMs + durationOffset,
519
632
  chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
520
- ...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
633
+ ...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
521
634
  ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
522
635
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
523
636
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -532,7 +645,7 @@ function mergeSynthesisResults(results, format) {
532
645
  ...bookmark,
533
646
  audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
534
647
  chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
535
- ...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
648
+ ...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
536
649
  ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
537
650
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
538
651
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -542,8 +655,9 @@ function mergeSynthesisResults(results, format) {
542
655
  durationOffset += Math.max(0, result.durationMs);
543
656
  }
544
657
  return {
545
- audioData: audioData.buffer,
658
+ audioData,
546
659
  durationMs: durationOffset,
660
+ mimeType: resolveMimeType(format),
547
661
  ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
548
662
  ...visemes.length > 0 ? { visemes } : {},
549
663
  ...bookmarks.length > 0 ? { bookmarks } : {},
@@ -551,46 +665,78 @@ function mergeSynthesisResults(results, format) {
551
665
  ...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
552
666
  };
553
667
  }
668
+ function mergeSynthesisResults(results, options) {
669
+ const resolvedOptions = typeof options === "string" ? { format: options } : options;
670
+ const format = resolvedOptions?.format;
671
+ if (!format) throw new UnsupportedMergeFormatError("");
672
+ const buffers = results.map((result) => result.audioData);
673
+ if (resolvedOptions.customMerger) {
674
+ return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
675
+ if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
676
+ return createMergedResult(results, merged, format);
677
+ }).catch((error) => {
678
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
679
+ throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
680
+ });
681
+ }
682
+ try {
683
+ return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
684
+ } catch (error) {
685
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
686
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
687
+ }
688
+ }
554
689
  async function synthesizeSpeech(ssml, config) {
555
690
  return (await synthesizeSsml(ssml, config)).audioData;
556
691
  }
557
692
 
558
693
  // src/safe.ts
559
- var import_ssml_core = require("@ssml-builder-js/ssml-core");
694
+ var import_ssml_core2 = require("@ssml-builder-js/ssml-core");
560
695
  var ChunkValidationError = class extends Error {
561
696
  constructor(chunkIndex, diagnostics) {
562
697
  super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
563
- this.kind = "chunk-validation";
698
+ this.kind = "validation-error";
564
699
  this.name = "ChunkValidationError";
565
700
  this.chunkIndex = chunkIndex;
566
701
  this.diagnostics = diagnostics;
567
702
  }
568
703
  };
704
+ function failure(error) {
705
+ return { ok: false, success: false, status: error.kind, error };
706
+ }
569
707
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
570
- const validationOptions = options.validation ?? options;
571
- const diagnostics = await Promise.resolve((0, import_ssml_core.validateAzureSsml)(ssml, validationOptions));
708
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
709
+ const diagnostics = await Promise.resolve((0, import_ssml_core2.validateAzureSsml)(ssml, validationOptions));
710
+ if (options.signal?.aborted) {
711
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
712
+ return failure(error);
713
+ }
572
714
  const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
573
715
  if (errors.length > 0) {
574
- return {
575
- ok: false,
576
- success: false,
577
- status: "validation-error",
578
- error: {
579
- kind: "validation",
580
- message: "SSML validation failed; the Azure Speech API was not called.",
581
- diagnostics: errors
582
- }
583
- };
716
+ return failure({
717
+ kind: "validation-error",
718
+ message: "SSML validation failed; the Azure Speech API was not called.",
719
+ diagnostics: errors
720
+ });
584
721
  }
585
722
  try {
586
- return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
723
+ return {
724
+ ok: true,
725
+ success: true,
726
+ status: "success",
727
+ value: await client.synthesizeSsml(ssml, { signal: options.signal })
728
+ };
587
729
  } catch (error) {
588
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
589
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
730
+ const synthesisError = toSynthesisError(error);
731
+ return failure(synthesisError);
590
732
  }
591
733
  }
592
734
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
593
- const validationOptions = options.validation ?? options;
735
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
736
+ if (options.signal?.aborted) {
737
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
738
+ return failure(error);
739
+ }
594
740
  const pending = (index, status, error) => {
595
741
  options.onProgress?.({
596
742
  currentChunk: status === "success" ? index + 1 : index,
@@ -609,7 +755,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
609
755
  const validations = await Promise.all(
610
756
  chunks.map(async (chunk) => {
611
757
  const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
612
- const diagnostics = await Promise.resolve((0, import_ssml_core.validateAzureSsml)(ssml, validationOptions));
758
+ const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
759
+ const diagnostics = await Promise.resolve(
760
+ (0, import_ssml_core2.validateAzureSsml)(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
761
+ );
613
762
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
614
763
  })
615
764
  );
@@ -617,31 +766,78 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
617
766
  if (firstInvalidIndex >= 0) {
618
767
  const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
619
768
  pending(firstInvalidIndex, "failed", error);
620
- return { ok: false, success: false, status: "validation-error", error };
769
+ return failure(error);
621
770
  }
622
771
  try {
623
772
  if (client.synthesizeChunks) {
624
- const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
773
+ const normalizedChunks = chunks.map((chunk) => {
774
+ if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
775
+ return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
776
+ });
777
+ const value = await client.synthesizeChunks(normalizedChunks, {
778
+ onProgress: options.onProgress,
779
+ outputFormat: options.outputFormat,
780
+ signal: options.signal,
781
+ timeoutMs: options.timeoutMs,
782
+ sourceNodePath: options.sourceNodePath
783
+ });
625
784
  return { ok: true, success: true, status: "success", value };
626
785
  }
627
786
  const results = [];
628
787
  for (const [index, chunk] of chunks.entries()) {
629
788
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
630
789
  const sourceNodePath = input.sourceNodePath;
790
+ const originalTextRange = input.originalTextRange;
631
791
  pending(index, "synthesizing");
632
792
  const startedAt = Date.now();
633
793
  try {
634
- const result = await client.synthesizeSsml(input.ssml);
794
+ const result = await client.synthesizeSsml(input.ssml, {
795
+ outputFormat: options.outputFormat,
796
+ signal: options.signal,
797
+ timeoutMs: options.timeoutMs,
798
+ sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
799
+ });
635
800
  results.push({
636
801
  ...result,
637
802
  ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
638
803
  ...sourceNodePath ? {
639
804
  boundaries: result.boundaries?.map((event) => ({
640
805
  ...event,
641
- sourceNodePath: [...sourceNodePath]
806
+ sourceNodePath: [...sourceNodePath],
807
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
642
808
  })),
643
- visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
644
- bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
809
+ visemes: result.visemes?.map((event) => ({
810
+ ...event,
811
+ sourceNodePath: [...sourceNodePath],
812
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
813
+ })),
814
+ bookmarks: result.bookmarks?.map((event) => ({
815
+ ...event,
816
+ sourceNodePath: [...sourceNodePath],
817
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
818
+ }))
819
+ } : {},
820
+ ...originalTextRange ? {
821
+ boundaries: result.boundaries?.map((event) => ({
822
+ ...event,
823
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
824
+ })),
825
+ wordBoundary: result.wordBoundary?.map((event) => ({
826
+ ...event,
827
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
828
+ })),
829
+ wordBoundaries: result.wordBoundaries?.map((event) => ({
830
+ ...event,
831
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
832
+ })),
833
+ visemes: result.visemes?.map((event) => ({
834
+ ...event,
835
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
836
+ })),
837
+ bookmarks: result.bookmarks?.map((event) => ({
838
+ ...event,
839
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
840
+ }))
645
841
  } : {}
646
842
  });
647
843
  options.onProgress?.({
@@ -671,13 +867,23 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
671
867
  ok: true,
672
868
  success: true,
673
869
  status: "success",
674
- value: mergeSynthesisResults(results, options.outputFormat)
870
+ value: mergeSynthesisResults(results, {
871
+ format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
872
+ })
675
873
  };
676
874
  } catch (error) {
677
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
678
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
875
+ const synthesisError = toSynthesisError(error);
876
+ return failure(synthesisError);
679
877
  }
680
878
  }
879
+ function withValidationSignal(options, signal) {
880
+ if (!signal) return options;
881
+ return {
882
+ ...options,
883
+ urlValidatorSignal: signal,
884
+ urlValidation: { ...options.urlValidation ?? {}, signal }
885
+ };
886
+ }
681
887
 
682
888
  // src/client.ts
683
889
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -694,11 +900,21 @@ var AzureTtsClient = class {
694
900
  const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
695
901
  return synthesizeSpeech(ssml, config);
696
902
  }
697
- async synthesizeSsml(ssml) {
903
+ async synthesizeSsml(ssml, options = {}) {
698
904
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
699
905
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
700
906
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
701
- return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
907
+ return synthesizeSsml(ssml, {
908
+ endpoint,
909
+ region,
910
+ subscriptionKey,
911
+ outputFormat: options.outputFormat ?? outputFormat,
912
+ signal: options.signal ?? signal,
913
+ timeoutMs: options.timeoutMs ?? timeoutMs,
914
+ sourceNodePath: options.sourceNodePath,
915
+ sourceTextSegments: options.sourceTextSegments,
916
+ sourceMarkers: options.sourceMarkers
917
+ });
702
918
  }
703
919
  async synthesizeChunks(chunks, options = {}) {
704
920
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
@@ -707,9 +923,10 @@ var AzureTtsClient = class {
707
923
  endpoint,
708
924
  region,
709
925
  subscriptionKey,
710
- outputFormat,
711
- signal,
712
- timeoutMs,
926
+ outputFormat: options.outputFormat ?? outputFormat,
927
+ signal: options.signal ?? signal,
928
+ timeoutMs: options.timeoutMs ?? timeoutMs,
929
+ sourceNodePath: options.sourceNodePath,
713
930
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
714
931
  });
715
932
  }
@@ -720,6 +937,8 @@ var AzureTtsClient = class {
720
937
  return synthesizeSsmlChunksSafe(this, chunks, {
721
938
  ...options,
722
939
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
940
+ signal: options.signal ?? __privateGet(this, _options).signal,
941
+ timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
723
942
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
724
943
  });
725
944
  }
@@ -821,12 +1040,17 @@ async function fetchAzureVoiceCatalog(options) {
821
1040
  AzureTtsError,
822
1041
  AzureTtsSdkError,
823
1042
  ChunkValidationError,
1043
+ DEFAULT_OUTPUT_FORMAT,
1044
+ MergeError,
1045
+ SynthesisCancelledError,
1046
+ SynthesisTimeoutError,
824
1047
  UnsupportedMergeFormatError,
825
1048
  canMergeAudioFormat,
826
1049
  fetchAzureVoiceCatalog,
827
1050
  mergeAudioBuffers,
828
1051
  mergeSynthesisResults,
829
1052
  resolveMergeAudioFormat,
1053
+ resolveMimeType,
830
1054
  synthesizeSpeech,
831
1055
  synthesizeSsml,
832
1056
  synthesizeSsmlChunks,