@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.mjs CHANGED
@@ -10,6 +10,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
10
10
  var AzureTtsError = class extends Error {
11
11
  constructor(status, statusText, responseBody, requestId) {
12
12
  super(`Azure TTS request failed: ${status} ${statusText}`);
13
+ this.kind = "azure-api-error";
13
14
  this.name = "AzureTtsError";
14
15
  this.status = status;
15
16
  this.statusText = statusText;
@@ -25,13 +26,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
25
26
  this.errorDetails = errorDetails;
26
27
  }
27
28
  };
29
+ var SynthesisCancelledError = class extends Error {
30
+ constructor(message = "Speech synthesis was cancelled.") {
31
+ super(message);
32
+ this.kind = "cancelled";
33
+ this.name = "SynthesisCancelledError";
34
+ }
35
+ };
36
+ var SynthesisTimeoutError = class extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.kind = "timeout";
40
+ this.name = "SynthesisTimeoutError";
41
+ }
42
+ };
43
+ var MergeError = class extends Error {
44
+ constructor(message, cause) {
45
+ super(message);
46
+ this.kind = "merge-error";
47
+ this.name = "MergeError";
48
+ this.cause = cause;
49
+ }
50
+ };
28
51
  var UnsupportedMergeFormatError = class extends Error {
29
52
  constructor(format) {
30
53
  super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
54
+ this.kind = "unsupported-format-error";
31
55
  this.name = "UnsupportedMergeFormatError";
32
56
  this.format = format;
33
57
  }
34
58
  };
59
+ function toSynthesisError(error) {
60
+ if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
61
+ return error;
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
64
+ if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
65
+ return createSpeechSdkError(error);
66
+ }
35
67
  function createSpeechSdkError(error) {
36
68
  const message = error instanceof Error ? error.message : String(error);
37
69
  return new AzureTtsSdkError(message);
@@ -39,9 +71,7 @@ function createSpeechSdkError(error) {
39
71
 
40
72
  // src/synthesis.ts
41
73
  import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
42
-
43
- // src/speechConfig.ts
44
- import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
74
+ import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
45
75
 
46
76
  // src/outputFormats.ts
47
77
  import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
@@ -87,6 +117,14 @@ var OUTPUT_FORMATS = {
87
117
  "amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
88
118
  "g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
89
119
  };
120
+ function resolveMimeType(outputFormat) {
121
+ if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
122
+ if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
123
+ if (/ogg/i.test(outputFormat)) return "audio/ogg";
124
+ if (/webm/i.test(outputFormat)) return "audio/webm";
125
+ if (/raw/i.test(outputFormat)) return "audio/L16";
126
+ return "application/octet-stream";
127
+ }
90
128
  function resolveOutputFormat(outputFormat) {
91
129
  const resolvedFormat = OUTPUT_FORMATS[outputFormat];
92
130
  if (resolvedFormat === void 0) {
@@ -96,6 +134,7 @@ function resolveOutputFormat(outputFormat) {
96
134
  }
97
135
 
98
136
  // src/speechConfig.ts
137
+ import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
99
138
  function resolveEndpoint(config) {
100
139
  const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
101
140
  return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
@@ -229,28 +268,35 @@ function resolveMergeAudioFormat(format) {
229
268
  function canMergeAudioFormat(format) {
230
269
  return resolveMergeAudioFormat(format) !== void 0;
231
270
  }
232
- function mergeAudioBuffers(buffers, format) {
233
- if (isWavFormat(format)) return mergeWavBuffers(buffers);
234
- if (isMp3Format(format)) {
235
- const parts = buffers.map(stripMp3Tags);
236
- const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
237
- let offset = 0;
238
- for (const part of parts) {
239
- output.set(part, offset);
240
- offset += part.byteLength;
271
+ function mergeAudioBuffers(buffers, options) {
272
+ const format = typeof options === "string" ? options : options?.format;
273
+ if (!format) throw new UnsupportedMergeFormatError("");
274
+ try {
275
+ if (isWavFormat(format)) return mergeWavBuffers(buffers);
276
+ if (isMp3Format(format)) {
277
+ const parts = buffers.map(stripMp3Tags);
278
+ const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
279
+ let offset = 0;
280
+ for (const part of parts) {
281
+ output.set(part, offset);
282
+ offset += part.byteLength;
283
+ }
284
+ return output.buffer;
241
285
  }
242
- return output.buffer;
243
- }
244
- if (isRawFormat(format)) {
245
- const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
246
- let offset = 0;
247
- for (const buffer of buffers) {
248
- output.set(new Uint8Array(buffer), offset);
249
- offset += buffer.byteLength;
286
+ if (isRawFormat(format)) {
287
+ const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
288
+ let offset = 0;
289
+ for (const buffer of buffers) {
290
+ output.set(new Uint8Array(buffer), offset);
291
+ offset += buffer.byteLength;
292
+ }
293
+ return output.buffer;
250
294
  }
251
- return output.buffer;
295
+ throw new UnsupportedMergeFormatError(format);
296
+ } catch (error) {
297
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
298
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
252
299
  }
253
- throw new UnsupportedMergeFormatError(format);
254
300
  }
255
301
  function closeSpeechResources(speechConfig, synthesizer) {
256
302
  try {
@@ -265,7 +311,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
265
311
  var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
266
312
  async function synthesizeSsml(ssml, config) {
267
313
  if (config.signal?.aborted) {
268
- throw createSpeechSdkError("Speech synthesis was cancelled.");
314
+ throw new SynthesisCancelledError();
269
315
  }
270
316
  const speechConfig = createSpeechConfig(config);
271
317
  const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
@@ -288,23 +334,94 @@ async function synthesizeSsml(ssml, config) {
288
334
  settled = true;
289
335
  cleanup();
290
336
  closeResources();
291
- reject(createSpeechSdkError(error));
337
+ reject(toSynthesisError(error));
292
338
  };
293
339
  const boundaries = [];
294
340
  const visemes = [];
295
341
  const bookmarks = [];
342
+ let sourceEventCursor = 0;
343
+ let generatedSourceMap;
344
+ if (!config.sourceTextSegments && !config.sourceMarkers) {
345
+ try {
346
+ generatedSourceMap = getSsmlSourceMap(ssml);
347
+ } catch {
348
+ generatedSourceMap = void 0;
349
+ }
350
+ }
351
+ const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
352
+ const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
353
+ ...segment,
354
+ range: {
355
+ start: segment.range.start + sourceBaseOffset,
356
+ end: segment.range.end + sourceBaseOffset
357
+ },
358
+ sourceNodePath: [...segment.sourceNodePath]
359
+ })) ?? [];
360
+ const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
361
+ ...marker,
362
+ originalTextRange: {
363
+ start: marker.originalTextRange.start + sourceBaseOffset,
364
+ end: marker.originalTextRange.end + sourceBaseOffset
365
+ },
366
+ sourceNodePath: [...marker.sourceNodePath]
367
+ })) ?? [];
368
+ const sourceText = sourceSegments.map((segment) => segment.text).join("");
369
+ const mapSourceEvent = (text, offsetHint, markerName) => {
370
+ const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
371
+ if (marker) {
372
+ return {
373
+ originalTextRange: { ...marker.originalTextRange },
374
+ sourceNodePath: [...marker.sourceNodePath],
375
+ textRange: { ...marker.originalTextRange }
376
+ };
377
+ }
378
+ if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
379
+ const value = text ?? "";
380
+ let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
381
+ if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
382
+ localStart = -1;
383
+ if (localStart < 0 || localStart > sourceText.length) {
384
+ localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
385
+ if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
386
+ }
387
+ localStart = Math.max(0, localStart);
388
+ const localEnd = Math.min(sourceText.length, localStart + value.length);
389
+ sourceEventCursor = Math.max(sourceEventCursor, localEnd);
390
+ const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
391
+ const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
392
+ 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);
393
+ return {
394
+ originalTextRange: { ...fallbackRange },
395
+ textRange: { ...fallbackRange },
396
+ ...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
397
+ };
398
+ };
296
399
  synthesizer.wordBoundary = (_sender, event) => {
297
400
  boundaries.push({
298
401
  text: event.text,
299
402
  audioOffsetMs: ticksToMilliseconds(event.audioOffset),
300
- durationMs: ticksToMilliseconds(event.duration)
403
+ durationMs: ticksToMilliseconds(event.duration),
404
+ ...mapSourceEvent(
405
+ event.text,
406
+ event.textOffset
407
+ )
301
408
  });
302
409
  };
303
410
  synthesizer.visemeReceived = (_sender, event) => {
304
- visemes.push({ visemeId: event.visemeId, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
411
+ const eventWithOffset = event;
412
+ visemes.push({
413
+ visemeId: event.visemeId,
414
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
415
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset)
416
+ });
305
417
  };
306
418
  synthesizer.bookmarkReached = (_sender, event) => {
307
- bookmarks.push({ name: event.text, audioOffsetMs: ticksToMilliseconds(event.audioOffset) });
419
+ const eventWithOffset = event;
420
+ bookmarks.push({
421
+ name: event.text,
422
+ audioOffsetMs: ticksToMilliseconds(event.audioOffset),
423
+ ...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
424
+ });
308
425
  };
309
426
  const cb = (result) => {
310
427
  if (settled) return;
@@ -327,8 +444,8 @@ async function synthesizeSsml(ssml, config) {
327
444
  const requestId = result.resultId;
328
445
  const addSourceMetadata = (event) => ({
329
446
  ...event,
330
- ...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
331
- ...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
447
+ ...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
448
+ ...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
332
449
  ...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
333
450
  ...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
334
451
  ...requestId ? { requestId } : {}
@@ -348,12 +465,12 @@ async function synthesizeSsml(ssml, config) {
348
465
  };
349
466
  try {
350
467
  if (config.signal) {
351
- abortHandler = () => rejectWithError("Speech synthesis was cancelled.");
468
+ abortHandler = () => rejectWithError(new SynthesisCancelledError());
352
469
  config.signal.addEventListener("abort", abortHandler, { once: true });
353
470
  }
354
471
  if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
355
472
  timeout = setTimeout(
356
- () => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
473
+ () => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
357
474
  config.timeoutMs
358
475
  );
359
476
  }
@@ -395,7 +512,9 @@ async function synthesizeSsmlChunks(chunks, config) {
395
512
  const result = await synthesizeSsml(input.ssml, {
396
513
  ...config,
397
514
  ...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
398
- ...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
515
+ ...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
516
+ ...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
517
+ ...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
399
518
  chunkIndex: index,
400
519
  onProgress: void 0
401
520
  });
@@ -423,27 +542,16 @@ async function synthesizeSsmlChunks(chunks, config) {
423
542
  throw error;
424
543
  }
425
544
  }
426
- return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
427
- }
428
- function mergeSynthesisResults(results, format) {
429
- const audioData = format ? new Uint8Array(
430
- mergeAudioBuffers(
431
- results.map((result) => result.audioData),
432
- format
433
- )
434
- ) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
435
- if (!format) {
436
- let offset = 0;
437
- for (const result of results) {
438
- audioData.set(new Uint8Array(result.audioData), offset);
439
- offset += result.audioData.byteLength;
440
- }
441
- }
545
+ return mergeSynthesisResults(results, {
546
+ format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
547
+ });
548
+ }
549
+ function createMergedResult(results, audioData, format) {
442
550
  const boundaries = [];
443
551
  const visemes = [];
444
552
  const bookmarks = [];
445
553
  let durationOffset = 0;
446
- for (const result of results) {
554
+ for (const [resultIndex, result] of results.entries()) {
447
555
  const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
448
556
  for (const boundary of chunkBoundaries) {
449
557
  const textRange = boundary.textRange ?? result.textRange;
@@ -453,7 +561,7 @@ function mergeSynthesisResults(results, format) {
453
561
  ...boundary,
454
562
  audioOffsetMs: boundary.audioOffsetMs + durationOffset,
455
563
  chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
456
- ...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
564
+ ...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
457
565
  ...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
458
566
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
459
567
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -468,7 +576,7 @@ function mergeSynthesisResults(results, format) {
468
576
  ...viseme,
469
577
  audioOffsetMs: viseme.audioOffsetMs + durationOffset,
470
578
  chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
471
- ...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
579
+ ...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
472
580
  ...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
473
581
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
474
582
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -483,7 +591,7 @@ function mergeSynthesisResults(results, format) {
483
591
  ...bookmark,
484
592
  audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
485
593
  chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
486
- ...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
594
+ ...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
487
595
  ...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
488
596
  ...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
489
597
  ...textRange ? { textRange: { ...textRange } } : {},
@@ -493,8 +601,9 @@ function mergeSynthesisResults(results, format) {
493
601
  durationOffset += Math.max(0, result.durationMs);
494
602
  }
495
603
  return {
496
- audioData: audioData.buffer,
604
+ audioData,
497
605
  durationMs: durationOffset,
606
+ mimeType: resolveMimeType(format),
498
607
  ...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
499
608
  ...visemes.length > 0 ? { visemes } : {},
500
609
  ...bookmarks.length > 0 ? { bookmarks } : {},
@@ -502,6 +611,27 @@ function mergeSynthesisResults(results, format) {
502
611
  ...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
503
612
  };
504
613
  }
614
+ function mergeSynthesisResults(results, options) {
615
+ const resolvedOptions = typeof options === "string" ? { format: options } : options;
616
+ const format = resolvedOptions?.format;
617
+ if (!format) throw new UnsupportedMergeFormatError("");
618
+ const buffers = results.map((result) => result.audioData);
619
+ if (resolvedOptions.customMerger) {
620
+ return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
621
+ if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
622
+ return createMergedResult(results, merged, format);
623
+ }).catch((error) => {
624
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
625
+ throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
626
+ });
627
+ }
628
+ try {
629
+ return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
630
+ } catch (error) {
631
+ if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
632
+ throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
633
+ }
634
+ }
505
635
  async function synthesizeSpeech(ssml, config) {
506
636
  return (await synthesizeSsml(ssml, config)).audioData;
507
637
  }
@@ -511,37 +641,48 @@ import { validateAzureSsml } from "@ssml-builder-js/ssml-core";
511
641
  var ChunkValidationError = class extends Error {
512
642
  constructor(chunkIndex, diagnostics) {
513
643
  super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
514
- this.kind = "chunk-validation";
644
+ this.kind = "validation-error";
515
645
  this.name = "ChunkValidationError";
516
646
  this.chunkIndex = chunkIndex;
517
647
  this.diagnostics = diagnostics;
518
648
  }
519
649
  };
650
+ function failure(error) {
651
+ return { ok: false, success: false, status: error.kind, error };
652
+ }
520
653
  async function synthesizeSsmlSafe(client, ssml, options = {}) {
521
- const validationOptions = options.validation ?? options;
654
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
522
655
  const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
656
+ if (options.signal?.aborted) {
657
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
658
+ return failure(error);
659
+ }
523
660
  const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
524
661
  if (errors.length > 0) {
525
- return {
526
- ok: false,
527
- success: false,
528
- status: "validation-error",
529
- error: {
530
- kind: "validation",
531
- message: "SSML validation failed; the Azure Speech API was not called.",
532
- diagnostics: errors
533
- }
534
- };
662
+ return failure({
663
+ kind: "validation-error",
664
+ message: "SSML validation failed; the Azure Speech API was not called.",
665
+ diagnostics: errors
666
+ });
535
667
  }
536
668
  try {
537
- return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
669
+ return {
670
+ ok: true,
671
+ success: true,
672
+ status: "success",
673
+ value: await client.synthesizeSsml(ssml, { signal: options.signal })
674
+ };
538
675
  } catch (error) {
539
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
540
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
676
+ const synthesisError = toSynthesisError(error);
677
+ return failure(synthesisError);
541
678
  }
542
679
  }
543
680
  async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
544
- const validationOptions = options.validation ?? options;
681
+ const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
682
+ if (options.signal?.aborted) {
683
+ const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
684
+ return failure(error);
685
+ }
545
686
  const pending = (index, status, error) => {
546
687
  options.onProgress?.({
547
688
  currentChunk: status === "success" ? index + 1 : index,
@@ -560,7 +701,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
560
701
  const validations = await Promise.all(
561
702
  chunks.map(async (chunk) => {
562
703
  const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
563
- const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
704
+ const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
705
+ const diagnostics = await Promise.resolve(
706
+ validateAzureSsml(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
707
+ );
564
708
  return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
565
709
  })
566
710
  );
@@ -568,31 +712,78 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
568
712
  if (firstInvalidIndex >= 0) {
569
713
  const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
570
714
  pending(firstInvalidIndex, "failed", error);
571
- return { ok: false, success: false, status: "validation-error", error };
715
+ return failure(error);
572
716
  }
573
717
  try {
574
718
  if (client.synthesizeChunks) {
575
- const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
719
+ const normalizedChunks = chunks.map((chunk) => {
720
+ if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
721
+ return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
722
+ });
723
+ const value = await client.synthesizeChunks(normalizedChunks, {
724
+ onProgress: options.onProgress,
725
+ outputFormat: options.outputFormat,
726
+ signal: options.signal,
727
+ timeoutMs: options.timeoutMs,
728
+ sourceNodePath: options.sourceNodePath
729
+ });
576
730
  return { ok: true, success: true, status: "success", value };
577
731
  }
578
732
  const results = [];
579
733
  for (const [index, chunk] of chunks.entries()) {
580
734
  const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
581
735
  const sourceNodePath = input.sourceNodePath;
736
+ const originalTextRange = input.originalTextRange;
582
737
  pending(index, "synthesizing");
583
738
  const startedAt = Date.now();
584
739
  try {
585
- const result = await client.synthesizeSsml(input.ssml);
740
+ const result = await client.synthesizeSsml(input.ssml, {
741
+ outputFormat: options.outputFormat,
742
+ signal: options.signal,
743
+ timeoutMs: options.timeoutMs,
744
+ sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
745
+ });
586
746
  results.push({
587
747
  ...result,
588
748
  ...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
589
749
  ...sourceNodePath ? {
590
750
  boundaries: result.boundaries?.map((event) => ({
591
751
  ...event,
592
- sourceNodePath: [...sourceNodePath]
752
+ sourceNodePath: [...sourceNodePath],
753
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
754
+ })),
755
+ visemes: result.visemes?.map((event) => ({
756
+ ...event,
757
+ sourceNodePath: [...sourceNodePath],
758
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
759
+ })),
760
+ bookmarks: result.bookmarks?.map((event) => ({
761
+ ...event,
762
+ sourceNodePath: [...sourceNodePath],
763
+ ...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
764
+ }))
765
+ } : {},
766
+ ...originalTextRange ? {
767
+ boundaries: result.boundaries?.map((event) => ({
768
+ ...event,
769
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
593
770
  })),
594
- visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
595
- bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
771
+ wordBoundary: result.wordBoundary?.map((event) => ({
772
+ ...event,
773
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
774
+ })),
775
+ wordBoundaries: result.wordBoundaries?.map((event) => ({
776
+ ...event,
777
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
778
+ })),
779
+ visemes: result.visemes?.map((event) => ({
780
+ ...event,
781
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
782
+ })),
783
+ bookmarks: result.bookmarks?.map((event) => ({
784
+ ...event,
785
+ originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
786
+ }))
596
787
  } : {}
597
788
  });
598
789
  options.onProgress?.({
@@ -622,13 +813,23 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
622
813
  ok: true,
623
814
  success: true,
624
815
  status: "success",
625
- value: mergeSynthesisResults(results, options.outputFormat)
816
+ value: mergeSynthesisResults(results, {
817
+ format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
818
+ })
626
819
  };
627
820
  } catch (error) {
628
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
629
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
821
+ const synthesisError = toSynthesisError(error);
822
+ return failure(synthesisError);
630
823
  }
631
824
  }
825
+ function withValidationSignal(options, signal) {
826
+ if (!signal) return options;
827
+ return {
828
+ ...options,
829
+ urlValidatorSignal: signal,
830
+ urlValidation: { ...options.urlValidation ?? {}, signal }
831
+ };
832
+ }
632
833
 
633
834
  // src/client.ts
634
835
  var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
@@ -645,11 +846,21 @@ var AzureTtsClient = class {
645
846
  const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
646
847
  return synthesizeSpeech(ssml, config);
647
848
  }
648
- async synthesizeSsml(ssml) {
849
+ async synthesizeSsml(ssml, options = {}) {
649
850
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
650
851
  const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
651
852
  __privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
652
- return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
853
+ return synthesizeSsml(ssml, {
854
+ endpoint,
855
+ region,
856
+ subscriptionKey,
857
+ outputFormat: options.outputFormat ?? outputFormat,
858
+ signal: options.signal ?? signal,
859
+ timeoutMs: options.timeoutMs ?? timeoutMs,
860
+ sourceNodePath: options.sourceNodePath,
861
+ sourceTextSegments: options.sourceTextSegments,
862
+ sourceMarkers: options.sourceMarkers
863
+ });
653
864
  }
654
865
  async synthesizeChunks(chunks, options = {}) {
655
866
  const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
@@ -658,9 +869,10 @@ var AzureTtsClient = class {
658
869
  endpoint,
659
870
  region,
660
871
  subscriptionKey,
661
- outputFormat,
662
- signal,
663
- timeoutMs,
872
+ outputFormat: options.outputFormat ?? outputFormat,
873
+ signal: options.signal ?? signal,
874
+ timeoutMs: options.timeoutMs ?? timeoutMs,
875
+ sourceNodePath: options.sourceNodePath,
664
876
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
665
877
  });
666
878
  }
@@ -671,6 +883,8 @@ var AzureTtsClient = class {
671
883
  return synthesizeSsmlChunksSafe(this, chunks, {
672
884
  ...options,
673
885
  outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
886
+ signal: options.signal ?? __privateGet(this, _options).signal,
887
+ timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
674
888
  onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
675
889
  });
676
890
  }
@@ -771,12 +985,17 @@ export {
771
985
  AzureTtsError,
772
986
  AzureTtsSdkError,
773
987
  ChunkValidationError,
988
+ DEFAULT_OUTPUT_FORMAT,
989
+ MergeError,
990
+ SynthesisCancelledError,
991
+ SynthesisTimeoutError,
774
992
  UnsupportedMergeFormatError,
775
993
  canMergeAudioFormat,
776
994
  fetchAzureVoiceCatalog,
777
995
  mergeAudioBuffers,
778
996
  mergeSynthesisResults,
779
997
  resolveMergeAudioFormat,
998
+ resolveMimeType,
780
999
  synthesizeSpeech,
781
1000
  synthesizeSsml,
782
1001
  synthesizeSsmlChunks,