@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/CHANGELOG.md +11 -0
- package/dist/index.d.mts +113 -17
- package/dist/index.d.ts +113 -17
- package/dist/index.js +310 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +19 -5
- package/src/errors.ts +63 -0
- package/src/index.ts +14 -2
- package/src/outputFormats.ts +14 -3
- package/src/safe.ts +160 -38
- package/src/synthesis.ts +215 -52
- package/src/types.ts +17 -1
- package/test/synthesis.test.ts +64 -0
- package/test/v213-pipeline.test.ts +23 -16
- package/test/v214-pipeline.test.ts +7 -3
- package/test/v215-pipeline.test.ts +104 -0
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,
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
output.
|
|
240
|
-
offset
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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 {
|
|
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
|
|
540
|
-
return
|
|
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
|
|
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
|
|
715
|
+
return failure(error);
|
|
572
716
|
}
|
|
573
717
|
try {
|
|
574
718
|
if (client.synthesizeChunks) {
|
|
575
|
-
const
|
|
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
|
-
|
|
595
|
-
|
|
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,
|
|
816
|
+
value: mergeSynthesisResults(results, {
|
|
817
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
818
|
+
})
|
|
626
819
|
};
|
|
627
820
|
} catch (error) {
|
|
628
|
-
const
|
|
629
|
-
return
|
|
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, {
|
|
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,
|