@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.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,
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
output.
|
|
289
|
-
offset
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 = "
|
|
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,
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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 {
|
|
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
|
|
589
|
-
return
|
|
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
|
|
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
|
|
769
|
+
return failure(error);
|
|
621
770
|
}
|
|
622
771
|
try {
|
|
623
772
|
if (client.synthesizeChunks) {
|
|
624
|
-
const
|
|
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) => ({
|
|
644
|
-
|
|
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,
|
|
870
|
+
value: mergeSynthesisResults(results, {
|
|
871
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
872
|
+
})
|
|
675
873
|
};
|
|
676
874
|
} catch (error) {
|
|
677
|
-
const
|
|
678
|
-
return
|
|
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, {
|
|
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,
|