@ssml-builder-js/azure-tts-client 2.13.0 → 2.14.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 +65 -9
- package/dist/index.d.ts +65 -9
- package/dist/index.js +363 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +357 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +20 -2
- package/src/errors.ts +11 -0
- package/src/index.ts +13 -3
- package/src/safe.ts +129 -1
- package/src/synthesis.ts +263 -17
- package/src/types.ts +32 -2
- package/src/voiceCatalog.ts +15 -0
- package/test/v213-pipeline.test.ts +4 -0
- package/test/v214-pipeline.test.ts +110 -0
package/dist/index.mjs
CHANGED
|
@@ -25,6 +25,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
25
25
|
this.errorDetails = errorDetails;
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
29
|
+
constructor(format) {
|
|
30
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
31
|
+
this.name = "UnsupportedMergeFormatError";
|
|
32
|
+
this.format = format;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
28
35
|
function createSpeechSdkError(error) {
|
|
29
36
|
const message = error instanceof Error ? error.message : String(error);
|
|
30
37
|
return new AzureTtsSdkError(message);
|
|
@@ -102,6 +109,149 @@ function createSpeechConfig(config) {
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
// src/synthesis.ts
|
|
112
|
+
function ascii(bytes, offset, value) {
|
|
113
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
114
|
+
}
|
|
115
|
+
function readUint32(bytes, offset) {
|
|
116
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
117
|
+
}
|
|
118
|
+
function parseWav(buffer) {
|
|
119
|
+
const bytes = new Uint8Array(buffer);
|
|
120
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
121
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
122
|
+
}
|
|
123
|
+
const chunks = [];
|
|
124
|
+
const dataParts = [];
|
|
125
|
+
let format;
|
|
126
|
+
let offset = 12;
|
|
127
|
+
while (offset < bytes.byteLength) {
|
|
128
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
129
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
130
|
+
const size = readUint32(bytes, offset + 4);
|
|
131
|
+
const dataStart = offset + 8;
|
|
132
|
+
const dataEnd = dataStart + size;
|
|
133
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
134
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
135
|
+
chunks.push({ id, data: data2 });
|
|
136
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
137
|
+
if (id === "data") dataParts.push(data2);
|
|
138
|
+
offset = dataEnd + (size & 1);
|
|
139
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
140
|
+
}
|
|
141
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
142
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
143
|
+
const data = new Uint8Array(dataLength);
|
|
144
|
+
let dataOffset = 0;
|
|
145
|
+
for (const part of dataParts) {
|
|
146
|
+
data.set(part, dataOffset);
|
|
147
|
+
dataOffset += part.byteLength;
|
|
148
|
+
}
|
|
149
|
+
return { chunks, data, format };
|
|
150
|
+
}
|
|
151
|
+
function writeUint32(target, offset, value) {
|
|
152
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
153
|
+
}
|
|
154
|
+
function writeChunk(target, offset, id, data) {
|
|
155
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
156
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
157
|
+
target.set(data, offset + 8);
|
|
158
|
+
const end = offset + 8 + data.byteLength;
|
|
159
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
160
|
+
return end + (data.byteLength & 1);
|
|
161
|
+
}
|
|
162
|
+
function mergeWavBuffers(buffers) {
|
|
163
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
164
|
+
const parsed = buffers.map(parseWav);
|
|
165
|
+
const first = parsed[0];
|
|
166
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
167
|
+
if (parsed.some(
|
|
168
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
169
|
+
))
|
|
170
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
171
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
172
|
+
const nonDataLength = first.chunks.reduce(
|
|
173
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
174
|
+
0
|
|
175
|
+
);
|
|
176
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
177
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
178
|
+
const output = new Uint8Array(outputLength);
|
|
179
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
180
|
+
writeUint32(output, 4, outputLength - 8);
|
|
181
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
182
|
+
let outputOffset = 12;
|
|
183
|
+
let dataWritten = false;
|
|
184
|
+
for (const chunk of first.chunks) {
|
|
185
|
+
if (chunk.id === "data") {
|
|
186
|
+
if (dataWritten) continue;
|
|
187
|
+
const data = new Uint8Array(dataLength);
|
|
188
|
+
let dataOffset = 0;
|
|
189
|
+
for (const item of parsed) {
|
|
190
|
+
data.set(item.data, dataOffset);
|
|
191
|
+
dataOffset += item.data.byteLength;
|
|
192
|
+
}
|
|
193
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
194
|
+
dataWritten = true;
|
|
195
|
+
} else {
|
|
196
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
200
|
+
return output.buffer;
|
|
201
|
+
}
|
|
202
|
+
function skipId3v2(bytes) {
|
|
203
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
204
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
205
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
206
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
207
|
+
}
|
|
208
|
+
function stripMp3Tags(buffer) {
|
|
209
|
+
const bytes = new Uint8Array(buffer);
|
|
210
|
+
const start = skipId3v2(bytes);
|
|
211
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
212
|
+
return bytes.slice(Math.min(start, end), end);
|
|
213
|
+
}
|
|
214
|
+
function isMp3Format(format) {
|
|
215
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
216
|
+
}
|
|
217
|
+
function isWavFormat(format) {
|
|
218
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
219
|
+
}
|
|
220
|
+
function isRawFormat(format) {
|
|
221
|
+
return /^raw(?:-|$)/i.test(format);
|
|
222
|
+
}
|
|
223
|
+
function resolveMergeAudioFormat(format) {
|
|
224
|
+
if (isWavFormat(format)) return "wav";
|
|
225
|
+
if (isMp3Format(format)) return "mp3";
|
|
226
|
+
if (isRawFormat(format)) return "raw";
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
function canMergeAudioFormat(format) {
|
|
230
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
231
|
+
}
|
|
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;
|
|
241
|
+
}
|
|
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;
|
|
250
|
+
}
|
|
251
|
+
return output.buffer;
|
|
252
|
+
}
|
|
253
|
+
throw new UnsupportedMergeFormatError(format);
|
|
254
|
+
}
|
|
105
255
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
106
256
|
try {
|
|
107
257
|
synthesizer.close();
|
|
@@ -178,6 +328,9 @@ async function synthesizeSsml(ssml, config) {
|
|
|
178
328
|
const addSourceMetadata = (event) => ({
|
|
179
329
|
...event,
|
|
180
330
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
331
|
+
...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
332
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
333
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
181
334
|
...requestId ? { requestId } : {}
|
|
182
335
|
});
|
|
183
336
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -213,60 +366,126 @@ async function synthesizeSsml(ssml, config) {
|
|
|
213
366
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
214
367
|
const results = [];
|
|
215
368
|
const totalChunks = chunks.length;
|
|
369
|
+
const report = (event) => config.onProgress?.(event);
|
|
216
370
|
for (const [index, chunk] of chunks.entries()) {
|
|
217
371
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
372
|
+
report({
|
|
373
|
+
currentChunk: index,
|
|
374
|
+
totalChunks,
|
|
375
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
376
|
+
chunkIndex: index,
|
|
377
|
+
originalTextRange: input.originalTextRange,
|
|
378
|
+
status: "pending",
|
|
379
|
+
durationMs: 0
|
|
222
380
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
381
|
+
}
|
|
382
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
383
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
384
|
+
report({
|
|
385
|
+
currentChunk: index,
|
|
226
386
|
totalChunks,
|
|
227
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
387
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
388
|
+
chunkIndex: index,
|
|
389
|
+
originalTextRange: input.originalTextRange,
|
|
390
|
+
status: "synthesizing",
|
|
391
|
+
durationMs: 0
|
|
228
392
|
});
|
|
393
|
+
const startedAt = Date.now();
|
|
394
|
+
try {
|
|
395
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
396
|
+
...config,
|
|
397
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
398
|
+
...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
|
|
399
|
+
chunkIndex: index,
|
|
400
|
+
onProgress: void 0
|
|
401
|
+
});
|
|
402
|
+
results.push(result);
|
|
403
|
+
report({
|
|
404
|
+
currentChunk: index + 1,
|
|
405
|
+
totalChunks,
|
|
406
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
407
|
+
chunkIndex: index,
|
|
408
|
+
originalTextRange: input.originalTextRange,
|
|
409
|
+
status: "success",
|
|
410
|
+
durationMs: Date.now() - startedAt
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
report({
|
|
414
|
+
currentChunk: index,
|
|
415
|
+
totalChunks,
|
|
416
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
417
|
+
chunkIndex: index,
|
|
418
|
+
originalTextRange: input.originalTextRange,
|
|
419
|
+
status: "failed",
|
|
420
|
+
durationMs: Date.now() - startedAt,
|
|
421
|
+
error
|
|
422
|
+
});
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
229
425
|
}
|
|
230
|
-
return mergeSynthesisResults(results);
|
|
426
|
+
return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
|
|
231
427
|
}
|
|
232
|
-
function mergeSynthesisResults(results) {
|
|
233
|
-
const
|
|
234
|
-
|
|
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
|
+
}
|
|
235
442
|
const boundaries = [];
|
|
236
443
|
const visemes = [];
|
|
237
444
|
const bookmarks = [];
|
|
238
|
-
let byteOffset = 0;
|
|
239
445
|
let durationOffset = 0;
|
|
240
446
|
for (const result of results) {
|
|
241
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
242
|
-
byteOffset += result.audioData.byteLength;
|
|
243
447
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
244
448
|
for (const boundary of chunkBoundaries) {
|
|
245
449
|
const textRange = boundary.textRange ?? result.textRange;
|
|
450
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
246
451
|
const requestId = boundary.requestId ?? result.requestId;
|
|
247
452
|
boundaries.push({
|
|
248
453
|
...boundary,
|
|
249
454
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
455
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
456
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
457
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
458
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
250
459
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
251
460
|
...requestId ? { requestId } : {}
|
|
252
461
|
});
|
|
253
462
|
}
|
|
254
463
|
for (const viseme of result.visemes ?? []) {
|
|
255
464
|
const textRange = viseme.textRange ?? result.textRange;
|
|
465
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
256
466
|
const requestId = viseme.requestId ?? result.requestId;
|
|
257
467
|
visemes.push({
|
|
258
468
|
...viseme,
|
|
259
469
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
470
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
471
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
472
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
473
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
260
474
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
261
475
|
...requestId ? { requestId } : {}
|
|
262
476
|
});
|
|
263
477
|
}
|
|
264
478
|
for (const bookmark of result.bookmarks ?? []) {
|
|
265
479
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
480
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
266
481
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
267
482
|
bookmarks.push({
|
|
268
483
|
...bookmark,
|
|
269
484
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
485
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
486
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
487
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
488
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
270
489
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
271
490
|
...requestId ? { requestId } : {}
|
|
272
491
|
});
|
|
@@ -289,6 +508,15 @@ async function synthesizeSpeech(ssml, config) {
|
|
|
289
508
|
|
|
290
509
|
// src/safe.ts
|
|
291
510
|
import { validateAzureSsml } from "@ssml-builder-js/ssml-core";
|
|
511
|
+
var ChunkValidationError = class extends Error {
|
|
512
|
+
constructor(chunkIndex, diagnostics) {
|
|
513
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
514
|
+
this.kind = "chunk-validation";
|
|
515
|
+
this.name = "ChunkValidationError";
|
|
516
|
+
this.chunkIndex = chunkIndex;
|
|
517
|
+
this.diagnostics = diagnostics;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
292
520
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
293
521
|
const validationOptions = options.validation ?? options;
|
|
294
522
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
@@ -312,6 +540,95 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
312
540
|
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
313
541
|
}
|
|
314
542
|
}
|
|
543
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
544
|
+
const validationOptions = options.validation ?? options;
|
|
545
|
+
const pending = (index, status, error) => {
|
|
546
|
+
options.onProgress?.({
|
|
547
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
548
|
+
totalChunks: chunks.length,
|
|
549
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
550
|
+
chunkIndex: index,
|
|
551
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
552
|
+
status,
|
|
553
|
+
durationMs: 0,
|
|
554
|
+
...error ? { error } : {}
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
chunks.forEach((_chunk, index) => {
|
|
558
|
+
pending(index, "pending");
|
|
559
|
+
});
|
|
560
|
+
const validations = await Promise.all(
|
|
561
|
+
chunks.map(async (chunk) => {
|
|
562
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
563
|
+
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
564
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
565
|
+
})
|
|
566
|
+
);
|
|
567
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
568
|
+
if (firstInvalidIndex >= 0) {
|
|
569
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
570
|
+
pending(firstInvalidIndex, "failed", error);
|
|
571
|
+
return { ok: false, success: false, status: "validation-error", error };
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
if (client.synthesizeChunks) {
|
|
575
|
+
const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
|
|
576
|
+
return { ok: true, success: true, status: "success", value };
|
|
577
|
+
}
|
|
578
|
+
const results = [];
|
|
579
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
580
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
581
|
+
const sourceNodePath = input.sourceNodePath;
|
|
582
|
+
pending(index, "synthesizing");
|
|
583
|
+
const startedAt = Date.now();
|
|
584
|
+
try {
|
|
585
|
+
const result = await client.synthesizeSsml(input.ssml);
|
|
586
|
+
results.push({
|
|
587
|
+
...result,
|
|
588
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
589
|
+
...sourceNodePath ? {
|
|
590
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
591
|
+
...event,
|
|
592
|
+
sourceNodePath: [...sourceNodePath]
|
|
593
|
+
})),
|
|
594
|
+
visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
595
|
+
bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
|
|
596
|
+
} : {}
|
|
597
|
+
});
|
|
598
|
+
options.onProgress?.({
|
|
599
|
+
currentChunk: index + 1,
|
|
600
|
+
totalChunks: chunks.length,
|
|
601
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
602
|
+
chunkIndex: index,
|
|
603
|
+
originalTextRange: input.originalTextRange,
|
|
604
|
+
status: "success",
|
|
605
|
+
durationMs: Date.now() - startedAt
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
options.onProgress?.({
|
|
609
|
+
currentChunk: index,
|
|
610
|
+
totalChunks: chunks.length,
|
|
611
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
612
|
+
chunkIndex: index,
|
|
613
|
+
originalTextRange: input.originalTextRange,
|
|
614
|
+
status: "failed",
|
|
615
|
+
durationMs: Date.now() - startedAt,
|
|
616
|
+
error
|
|
617
|
+
});
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
success: true,
|
|
624
|
+
status: "success",
|
|
625
|
+
value: mergeSynthesisResults(results, options.outputFormat)
|
|
626
|
+
};
|
|
627
|
+
} catch (error) {
|
|
628
|
+
const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
|
|
629
|
+
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
315
632
|
|
|
316
633
|
// src/client.ts
|
|
317
634
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -350,6 +667,16 @@ var AzureTtsClient = class {
|
|
|
350
667
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
351
668
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
352
669
|
}
|
|
670
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
671
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
672
|
+
...options,
|
|
673
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
674
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
678
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
679
|
+
}
|
|
353
680
|
};
|
|
354
681
|
_options = new WeakMap();
|
|
355
682
|
|
|
@@ -405,6 +732,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
405
732
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
406
733
|
const styles = stringList(record.StyleList);
|
|
407
734
|
const status = normalizeStatus(record.Status);
|
|
735
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
736
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
737
|
+
const models = stringList(record.Models);
|
|
408
738
|
const merged = {
|
|
409
739
|
name: existing?.name ?? name,
|
|
410
740
|
locale: existing?.locale ?? locale,
|
|
@@ -414,6 +744,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
414
744
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
415
745
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
416
746
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
747
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
748
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
749
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
750
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
751
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
752
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
417
753
|
if (status) merged.status = status;
|
|
418
754
|
else if (existing?.status) merged.status = existing.status;
|
|
419
755
|
voices.set(key, merged);
|
|
@@ -434,11 +770,17 @@ export {
|
|
|
434
770
|
AzureTtsClient,
|
|
435
771
|
AzureTtsError,
|
|
436
772
|
AzureTtsSdkError,
|
|
773
|
+
ChunkValidationError,
|
|
774
|
+
UnsupportedMergeFormatError,
|
|
775
|
+
canMergeAudioFormat,
|
|
437
776
|
fetchAzureVoiceCatalog,
|
|
777
|
+
mergeAudioBuffers,
|
|
438
778
|
mergeSynthesisResults,
|
|
779
|
+
resolveMergeAudioFormat,
|
|
439
780
|
synthesizeSpeech,
|
|
440
781
|
synthesizeSsml,
|
|
441
782
|
synthesizeSsmlChunks,
|
|
783
|
+
synthesizeSsmlChunksSafe,
|
|
442
784
|
synthesizeSsmlSafe
|
|
443
785
|
};
|
|
444
786
|
//# sourceMappingURL=index.mjs.map
|