@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.js
CHANGED
|
@@ -40,11 +40,17 @@ __export(index_exports, {
|
|
|
40
40
|
AzureTtsClient: () => AzureTtsClient,
|
|
41
41
|
AzureTtsError: () => AzureTtsError,
|
|
42
42
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
43
|
+
ChunkValidationError: () => ChunkValidationError,
|
|
44
|
+
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
45
|
+
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
43
46
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
47
|
+
mergeAudioBuffers: () => mergeAudioBuffers,
|
|
44
48
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
49
|
+
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
45
50
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
46
51
|
synthesizeSsml: () => synthesizeSsml,
|
|
47
52
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
53
|
+
synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
|
|
48
54
|
synthesizeSsmlSafe: () => synthesizeSsmlSafe
|
|
49
55
|
});
|
|
50
56
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -68,6 +74,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
68
74
|
this.errorDetails = errorDetails;
|
|
69
75
|
}
|
|
70
76
|
};
|
|
77
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
78
|
+
constructor(format) {
|
|
79
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
80
|
+
this.name = "UnsupportedMergeFormatError";
|
|
81
|
+
this.format = format;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
71
84
|
function createSpeechSdkError(error) {
|
|
72
85
|
const message = error instanceof Error ? error.message : String(error);
|
|
73
86
|
return new AzureTtsSdkError(message);
|
|
@@ -145,6 +158,149 @@ function createSpeechConfig(config) {
|
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
// src/synthesis.ts
|
|
161
|
+
function ascii(bytes, offset, value) {
|
|
162
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
163
|
+
}
|
|
164
|
+
function readUint32(bytes, offset) {
|
|
165
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
166
|
+
}
|
|
167
|
+
function parseWav(buffer) {
|
|
168
|
+
const bytes = new Uint8Array(buffer);
|
|
169
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
170
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
171
|
+
}
|
|
172
|
+
const chunks = [];
|
|
173
|
+
const dataParts = [];
|
|
174
|
+
let format;
|
|
175
|
+
let offset = 12;
|
|
176
|
+
while (offset < bytes.byteLength) {
|
|
177
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
178
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
179
|
+
const size = readUint32(bytes, offset + 4);
|
|
180
|
+
const dataStart = offset + 8;
|
|
181
|
+
const dataEnd = dataStart + size;
|
|
182
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
183
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
184
|
+
chunks.push({ id, data: data2 });
|
|
185
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
186
|
+
if (id === "data") dataParts.push(data2);
|
|
187
|
+
offset = dataEnd + (size & 1);
|
|
188
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
189
|
+
}
|
|
190
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
191
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
192
|
+
const data = new Uint8Array(dataLength);
|
|
193
|
+
let dataOffset = 0;
|
|
194
|
+
for (const part of dataParts) {
|
|
195
|
+
data.set(part, dataOffset);
|
|
196
|
+
dataOffset += part.byteLength;
|
|
197
|
+
}
|
|
198
|
+
return { chunks, data, format };
|
|
199
|
+
}
|
|
200
|
+
function writeUint32(target, offset, value) {
|
|
201
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
202
|
+
}
|
|
203
|
+
function writeChunk(target, offset, id, data) {
|
|
204
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
205
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
206
|
+
target.set(data, offset + 8);
|
|
207
|
+
const end = offset + 8 + data.byteLength;
|
|
208
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
209
|
+
return end + (data.byteLength & 1);
|
|
210
|
+
}
|
|
211
|
+
function mergeWavBuffers(buffers) {
|
|
212
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
213
|
+
const parsed = buffers.map(parseWav);
|
|
214
|
+
const first = parsed[0];
|
|
215
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
216
|
+
if (parsed.some(
|
|
217
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
218
|
+
))
|
|
219
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
220
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
221
|
+
const nonDataLength = first.chunks.reduce(
|
|
222
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
223
|
+
0
|
|
224
|
+
);
|
|
225
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
226
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
227
|
+
const output = new Uint8Array(outputLength);
|
|
228
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
229
|
+
writeUint32(output, 4, outputLength - 8);
|
|
230
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
231
|
+
let outputOffset = 12;
|
|
232
|
+
let dataWritten = false;
|
|
233
|
+
for (const chunk of first.chunks) {
|
|
234
|
+
if (chunk.id === "data") {
|
|
235
|
+
if (dataWritten) continue;
|
|
236
|
+
const data = new Uint8Array(dataLength);
|
|
237
|
+
let dataOffset = 0;
|
|
238
|
+
for (const item of parsed) {
|
|
239
|
+
data.set(item.data, dataOffset);
|
|
240
|
+
dataOffset += item.data.byteLength;
|
|
241
|
+
}
|
|
242
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
243
|
+
dataWritten = true;
|
|
244
|
+
} else {
|
|
245
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
249
|
+
return output.buffer;
|
|
250
|
+
}
|
|
251
|
+
function skipId3v2(bytes) {
|
|
252
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
253
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
254
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
255
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
256
|
+
}
|
|
257
|
+
function stripMp3Tags(buffer) {
|
|
258
|
+
const bytes = new Uint8Array(buffer);
|
|
259
|
+
const start = skipId3v2(bytes);
|
|
260
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
261
|
+
return bytes.slice(Math.min(start, end), end);
|
|
262
|
+
}
|
|
263
|
+
function isMp3Format(format) {
|
|
264
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
265
|
+
}
|
|
266
|
+
function isWavFormat(format) {
|
|
267
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
268
|
+
}
|
|
269
|
+
function isRawFormat(format) {
|
|
270
|
+
return /^raw(?:-|$)/i.test(format);
|
|
271
|
+
}
|
|
272
|
+
function resolveMergeAudioFormat(format) {
|
|
273
|
+
if (isWavFormat(format)) return "wav";
|
|
274
|
+
if (isMp3Format(format)) return "mp3";
|
|
275
|
+
if (isRawFormat(format)) return "raw";
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
function canMergeAudioFormat(format) {
|
|
279
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
280
|
+
}
|
|
281
|
+
function mergeAudioBuffers(buffers, format) {
|
|
282
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
283
|
+
if (isMp3Format(format)) {
|
|
284
|
+
const parts = buffers.map(stripMp3Tags);
|
|
285
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
286
|
+
let offset = 0;
|
|
287
|
+
for (const part of parts) {
|
|
288
|
+
output.set(part, offset);
|
|
289
|
+
offset += part.byteLength;
|
|
290
|
+
}
|
|
291
|
+
return output.buffer;
|
|
292
|
+
}
|
|
293
|
+
if (isRawFormat(format)) {
|
|
294
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
295
|
+
let offset = 0;
|
|
296
|
+
for (const buffer of buffers) {
|
|
297
|
+
output.set(new Uint8Array(buffer), offset);
|
|
298
|
+
offset += buffer.byteLength;
|
|
299
|
+
}
|
|
300
|
+
return output.buffer;
|
|
301
|
+
}
|
|
302
|
+
throw new UnsupportedMergeFormatError(format);
|
|
303
|
+
}
|
|
148
304
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
149
305
|
try {
|
|
150
306
|
synthesizer.close();
|
|
@@ -221,6 +377,9 @@ async function synthesizeSsml(ssml, config) {
|
|
|
221
377
|
const addSourceMetadata = (event) => ({
|
|
222
378
|
...event,
|
|
223
379
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
380
|
+
...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
381
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
382
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
224
383
|
...requestId ? { requestId } : {}
|
|
225
384
|
});
|
|
226
385
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -256,60 +415,126 @@ async function synthesizeSsml(ssml, config) {
|
|
|
256
415
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
257
416
|
const results = [];
|
|
258
417
|
const totalChunks = chunks.length;
|
|
418
|
+
const report = (event) => config.onProgress?.(event);
|
|
259
419
|
for (const [index, chunk] of chunks.entries()) {
|
|
260
420
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
421
|
+
report({
|
|
422
|
+
currentChunk: index,
|
|
423
|
+
totalChunks,
|
|
424
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
425
|
+
chunkIndex: index,
|
|
426
|
+
originalTextRange: input.originalTextRange,
|
|
427
|
+
status: "pending",
|
|
428
|
+
durationMs: 0
|
|
265
429
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
430
|
+
}
|
|
431
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
432
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
433
|
+
report({
|
|
434
|
+
currentChunk: index,
|
|
269
435
|
totalChunks,
|
|
270
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
436
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
437
|
+
chunkIndex: index,
|
|
438
|
+
originalTextRange: input.originalTextRange,
|
|
439
|
+
status: "synthesizing",
|
|
440
|
+
durationMs: 0
|
|
271
441
|
});
|
|
442
|
+
const startedAt = Date.now();
|
|
443
|
+
try {
|
|
444
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
445
|
+
...config,
|
|
446
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
447
|
+
...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
|
|
448
|
+
chunkIndex: index,
|
|
449
|
+
onProgress: void 0
|
|
450
|
+
});
|
|
451
|
+
results.push(result);
|
|
452
|
+
report({
|
|
453
|
+
currentChunk: index + 1,
|
|
454
|
+
totalChunks,
|
|
455
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
456
|
+
chunkIndex: index,
|
|
457
|
+
originalTextRange: input.originalTextRange,
|
|
458
|
+
status: "success",
|
|
459
|
+
durationMs: Date.now() - startedAt
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
report({
|
|
463
|
+
currentChunk: index,
|
|
464
|
+
totalChunks,
|
|
465
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
466
|
+
chunkIndex: index,
|
|
467
|
+
originalTextRange: input.originalTextRange,
|
|
468
|
+
status: "failed",
|
|
469
|
+
durationMs: Date.now() - startedAt,
|
|
470
|
+
error
|
|
471
|
+
});
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
272
474
|
}
|
|
273
|
-
return mergeSynthesisResults(results);
|
|
475
|
+
return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
|
|
274
476
|
}
|
|
275
|
-
function mergeSynthesisResults(results) {
|
|
276
|
-
const
|
|
277
|
-
|
|
477
|
+
function mergeSynthesisResults(results, format) {
|
|
478
|
+
const audioData = format ? new Uint8Array(
|
|
479
|
+
mergeAudioBuffers(
|
|
480
|
+
results.map((result) => result.audioData),
|
|
481
|
+
format
|
|
482
|
+
)
|
|
483
|
+
) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
|
|
484
|
+
if (!format) {
|
|
485
|
+
let offset = 0;
|
|
486
|
+
for (const result of results) {
|
|
487
|
+
audioData.set(new Uint8Array(result.audioData), offset);
|
|
488
|
+
offset += result.audioData.byteLength;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
278
491
|
const boundaries = [];
|
|
279
492
|
const visemes = [];
|
|
280
493
|
const bookmarks = [];
|
|
281
|
-
let byteOffset = 0;
|
|
282
494
|
let durationOffset = 0;
|
|
283
495
|
for (const result of results) {
|
|
284
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
285
|
-
byteOffset += result.audioData.byteLength;
|
|
286
496
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
287
497
|
for (const boundary of chunkBoundaries) {
|
|
288
498
|
const textRange = boundary.textRange ?? result.textRange;
|
|
499
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
289
500
|
const requestId = boundary.requestId ?? result.requestId;
|
|
290
501
|
boundaries.push({
|
|
291
502
|
...boundary,
|
|
292
503
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
504
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
505
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
506
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
507
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
293
508
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
294
509
|
...requestId ? { requestId } : {}
|
|
295
510
|
});
|
|
296
511
|
}
|
|
297
512
|
for (const viseme of result.visemes ?? []) {
|
|
298
513
|
const textRange = viseme.textRange ?? result.textRange;
|
|
514
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
299
515
|
const requestId = viseme.requestId ?? result.requestId;
|
|
300
516
|
visemes.push({
|
|
301
517
|
...viseme,
|
|
302
518
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
519
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
520
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
521
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
522
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
303
523
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
304
524
|
...requestId ? { requestId } : {}
|
|
305
525
|
});
|
|
306
526
|
}
|
|
307
527
|
for (const bookmark of result.bookmarks ?? []) {
|
|
308
528
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
529
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
309
530
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
310
531
|
bookmarks.push({
|
|
311
532
|
...bookmark,
|
|
312
533
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
534
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
535
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
536
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
537
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
313
538
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
314
539
|
...requestId ? { requestId } : {}
|
|
315
540
|
});
|
|
@@ -332,6 +557,15 @@ async function synthesizeSpeech(ssml, config) {
|
|
|
332
557
|
|
|
333
558
|
// src/safe.ts
|
|
334
559
|
var import_ssml_core = require("@ssml-builder-js/ssml-core");
|
|
560
|
+
var ChunkValidationError = class extends Error {
|
|
561
|
+
constructor(chunkIndex, diagnostics) {
|
|
562
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
563
|
+
this.kind = "chunk-validation";
|
|
564
|
+
this.name = "ChunkValidationError";
|
|
565
|
+
this.chunkIndex = chunkIndex;
|
|
566
|
+
this.diagnostics = diagnostics;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
335
569
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
336
570
|
const validationOptions = options.validation ?? options;
|
|
337
571
|
const diagnostics = await Promise.resolve((0, import_ssml_core.validateAzureSsml)(ssml, validationOptions));
|
|
@@ -355,6 +589,95 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
355
589
|
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
356
590
|
}
|
|
357
591
|
}
|
|
592
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
593
|
+
const validationOptions = options.validation ?? options;
|
|
594
|
+
const pending = (index, status, error) => {
|
|
595
|
+
options.onProgress?.({
|
|
596
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
597
|
+
totalChunks: chunks.length,
|
|
598
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
599
|
+
chunkIndex: index,
|
|
600
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
601
|
+
status,
|
|
602
|
+
durationMs: 0,
|
|
603
|
+
...error ? { error } : {}
|
|
604
|
+
});
|
|
605
|
+
};
|
|
606
|
+
chunks.forEach((_chunk, index) => {
|
|
607
|
+
pending(index, "pending");
|
|
608
|
+
});
|
|
609
|
+
const validations = await Promise.all(
|
|
610
|
+
chunks.map(async (chunk) => {
|
|
611
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
612
|
+
const diagnostics = await Promise.resolve((0, import_ssml_core.validateAzureSsml)(ssml, validationOptions));
|
|
613
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
614
|
+
})
|
|
615
|
+
);
|
|
616
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
617
|
+
if (firstInvalidIndex >= 0) {
|
|
618
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
619
|
+
pending(firstInvalidIndex, "failed", error);
|
|
620
|
+
return { ok: false, success: false, status: "validation-error", error };
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
if (client.synthesizeChunks) {
|
|
624
|
+
const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
|
|
625
|
+
return { ok: true, success: true, status: "success", value };
|
|
626
|
+
}
|
|
627
|
+
const results = [];
|
|
628
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
629
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
630
|
+
const sourceNodePath = input.sourceNodePath;
|
|
631
|
+
pending(index, "synthesizing");
|
|
632
|
+
const startedAt = Date.now();
|
|
633
|
+
try {
|
|
634
|
+
const result = await client.synthesizeSsml(input.ssml);
|
|
635
|
+
results.push({
|
|
636
|
+
...result,
|
|
637
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
638
|
+
...sourceNodePath ? {
|
|
639
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
640
|
+
...event,
|
|
641
|
+
sourceNodePath: [...sourceNodePath]
|
|
642
|
+
})),
|
|
643
|
+
visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
644
|
+
bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
|
|
645
|
+
} : {}
|
|
646
|
+
});
|
|
647
|
+
options.onProgress?.({
|
|
648
|
+
currentChunk: index + 1,
|
|
649
|
+
totalChunks: chunks.length,
|
|
650
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
651
|
+
chunkIndex: index,
|
|
652
|
+
originalTextRange: input.originalTextRange,
|
|
653
|
+
status: "success",
|
|
654
|
+
durationMs: Date.now() - startedAt
|
|
655
|
+
});
|
|
656
|
+
} catch (error) {
|
|
657
|
+
options.onProgress?.({
|
|
658
|
+
currentChunk: index,
|
|
659
|
+
totalChunks: chunks.length,
|
|
660
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
661
|
+
chunkIndex: index,
|
|
662
|
+
originalTextRange: input.originalTextRange,
|
|
663
|
+
status: "failed",
|
|
664
|
+
durationMs: Date.now() - startedAt,
|
|
665
|
+
error
|
|
666
|
+
});
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
success: true,
|
|
673
|
+
status: "success",
|
|
674
|
+
value: mergeSynthesisResults(results, options.outputFormat)
|
|
675
|
+
};
|
|
676
|
+
} catch (error) {
|
|
677
|
+
const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
|
|
678
|
+
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
358
681
|
|
|
359
682
|
// src/client.ts
|
|
360
683
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -393,6 +716,16 @@ var AzureTtsClient = class {
|
|
|
393
716
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
394
717
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
395
718
|
}
|
|
719
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
720
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
721
|
+
...options,
|
|
722
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
723
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
727
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
728
|
+
}
|
|
396
729
|
};
|
|
397
730
|
_options = new WeakMap();
|
|
398
731
|
|
|
@@ -448,6 +781,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
448
781
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
449
782
|
const styles = stringList(record.StyleList);
|
|
450
783
|
const status = normalizeStatus(record.Status);
|
|
784
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
785
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
786
|
+
const models = stringList(record.Models);
|
|
451
787
|
const merged = {
|
|
452
788
|
name: existing?.name ?? name,
|
|
453
789
|
locale: existing?.locale ?? locale,
|
|
@@ -457,6 +793,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
457
793
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
458
794
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
459
795
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
796
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
797
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
798
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
799
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
800
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
801
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
460
802
|
if (status) merged.status = status;
|
|
461
803
|
else if (existing?.status) merged.status = existing.status;
|
|
462
804
|
voices.set(key, merged);
|
|
@@ -478,11 +820,17 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
478
820
|
AzureTtsClient,
|
|
479
821
|
AzureTtsError,
|
|
480
822
|
AzureTtsSdkError,
|
|
823
|
+
ChunkValidationError,
|
|
824
|
+
UnsupportedMergeFormatError,
|
|
825
|
+
canMergeAudioFormat,
|
|
481
826
|
fetchAzureVoiceCatalog,
|
|
827
|
+
mergeAudioBuffers,
|
|
482
828
|
mergeSynthesisResults,
|
|
829
|
+
resolveMergeAudioFormat,
|
|
483
830
|
synthesizeSpeech,
|
|
484
831
|
synthesizeSsml,
|
|
485
832
|
synthesizeSsmlChunks,
|
|
833
|
+
synthesizeSsmlChunksSafe,
|
|
486
834
|
synthesizeSsmlSafe
|
|
487
835
|
});
|
|
488
836
|
//# sourceMappingURL=index.js.map
|