ssml-builder-js 2.12.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/README.md +34 -1
- package/dist/{chunk-4BVNAUVR.mjs → chunk-AQ55MOPU.mjs} +246 -21
- package/dist/chunk-AQ55MOPU.mjs.map +1 -0
- package/dist/chunk-QFIBPCO4.mjs +1816 -0
- package/dist/chunk-QFIBPCO4.mjs.map +1 -0
- package/dist/{chunk-FHDFRM2F.mjs → chunk-UZSCXPC5.mjs} +248 -1722
- package/dist/chunk-UZSCXPC5.mjs.map +1 -0
- package/dist/core.d.mts +62 -3
- package/dist/core.d.ts +62 -3
- package/dist/core.js +246 -20
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +196 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +6 -4
- package/dist/elements.mjs.map +1 -1
- package/dist/index.d-CUXRwSw0.d.mts +232 -0
- package/dist/index.d-CUXRwSw0.d.ts +232 -0
- package/dist/index.d.mts +151 -2
- package/dist/index.d.ts +151 -2
- package/dist/index.js +2264 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +479 -4
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +65 -163
- package/dist/react.d.ts +65 -163
- package/dist/react.js +498 -22
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +162 -18
- package/dist/react.mjs.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-4BVNAUVR.mjs.map +0 -1
- package/dist/chunk-FHDFRM2F.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
areAzureLanguagesEquivalent,
|
|
3
3
|
buildPartialSsml,
|
|
4
4
|
buildSsml,
|
|
5
|
+
createAzureUrlValidatorRunner,
|
|
5
6
|
extractSsmlText,
|
|
6
7
|
extractSsmlTranslatableText,
|
|
7
8
|
fromPlainTextToSsml,
|
|
@@ -15,7 +16,10 @@ import {
|
|
|
15
16
|
validateAzureSsml,
|
|
16
17
|
validateSsml,
|
|
17
18
|
validateSsmlStructureIntegrity
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-AQ55MOPU.mjs";
|
|
20
|
+
import {
|
|
21
|
+
validateAzureSsml as validateAzureSsml2
|
|
22
|
+
} from "./chunk-QFIBPCO4.mjs";
|
|
19
23
|
import {
|
|
20
24
|
__privateAdd,
|
|
21
25
|
__privateGet,
|
|
@@ -41,6 +45,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
41
45
|
this.errorDetails = errorDetails;
|
|
42
46
|
}
|
|
43
47
|
};
|
|
48
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
49
|
+
constructor(format) {
|
|
50
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
51
|
+
this.name = "UnsupportedMergeFormatError";
|
|
52
|
+
this.format = format;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
44
55
|
function createSpeechSdkError(error) {
|
|
45
56
|
const message = error instanceof Error ? error.message : String(error);
|
|
46
57
|
return new AzureTtsSdkError(message);
|
|
@@ -118,6 +129,149 @@ function createSpeechConfig(config) {
|
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
// packages/azure-tts-client/src/synthesis.ts
|
|
132
|
+
function ascii(bytes, offset, value) {
|
|
133
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
134
|
+
}
|
|
135
|
+
function readUint32(bytes, offset) {
|
|
136
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
137
|
+
}
|
|
138
|
+
function parseWav(buffer) {
|
|
139
|
+
const bytes = new Uint8Array(buffer);
|
|
140
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
141
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
142
|
+
}
|
|
143
|
+
const chunks = [];
|
|
144
|
+
const dataParts = [];
|
|
145
|
+
let format;
|
|
146
|
+
let offset = 12;
|
|
147
|
+
while (offset < bytes.byteLength) {
|
|
148
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
149
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
150
|
+
const size = readUint32(bytes, offset + 4);
|
|
151
|
+
const dataStart = offset + 8;
|
|
152
|
+
const dataEnd = dataStart + size;
|
|
153
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
154
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
155
|
+
chunks.push({ id, data: data2 });
|
|
156
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
157
|
+
if (id === "data") dataParts.push(data2);
|
|
158
|
+
offset = dataEnd + (size & 1);
|
|
159
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
160
|
+
}
|
|
161
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
162
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
163
|
+
const data = new Uint8Array(dataLength);
|
|
164
|
+
let dataOffset = 0;
|
|
165
|
+
for (const part of dataParts) {
|
|
166
|
+
data.set(part, dataOffset);
|
|
167
|
+
dataOffset += part.byteLength;
|
|
168
|
+
}
|
|
169
|
+
return { chunks, data, format };
|
|
170
|
+
}
|
|
171
|
+
function writeUint32(target, offset, value) {
|
|
172
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
173
|
+
}
|
|
174
|
+
function writeChunk(target, offset, id, data) {
|
|
175
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
176
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
177
|
+
target.set(data, offset + 8);
|
|
178
|
+
const end = offset + 8 + data.byteLength;
|
|
179
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
180
|
+
return end + (data.byteLength & 1);
|
|
181
|
+
}
|
|
182
|
+
function mergeWavBuffers(buffers) {
|
|
183
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
184
|
+
const parsed = buffers.map(parseWav);
|
|
185
|
+
const first = parsed[0];
|
|
186
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
187
|
+
if (parsed.some(
|
|
188
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
189
|
+
))
|
|
190
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
191
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
192
|
+
const nonDataLength = first.chunks.reduce(
|
|
193
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
194
|
+
0
|
|
195
|
+
);
|
|
196
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
197
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
198
|
+
const output = new Uint8Array(outputLength);
|
|
199
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
200
|
+
writeUint32(output, 4, outputLength - 8);
|
|
201
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
202
|
+
let outputOffset = 12;
|
|
203
|
+
let dataWritten = false;
|
|
204
|
+
for (const chunk of first.chunks) {
|
|
205
|
+
if (chunk.id === "data") {
|
|
206
|
+
if (dataWritten) continue;
|
|
207
|
+
const data = new Uint8Array(dataLength);
|
|
208
|
+
let dataOffset = 0;
|
|
209
|
+
for (const item of parsed) {
|
|
210
|
+
data.set(item.data, dataOffset);
|
|
211
|
+
dataOffset += item.data.byteLength;
|
|
212
|
+
}
|
|
213
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
214
|
+
dataWritten = true;
|
|
215
|
+
} else {
|
|
216
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
220
|
+
return output.buffer;
|
|
221
|
+
}
|
|
222
|
+
function skipId3v2(bytes) {
|
|
223
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
224
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
225
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
226
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
227
|
+
}
|
|
228
|
+
function stripMp3Tags(buffer) {
|
|
229
|
+
const bytes = new Uint8Array(buffer);
|
|
230
|
+
const start = skipId3v2(bytes);
|
|
231
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
232
|
+
return bytes.slice(Math.min(start, end), end);
|
|
233
|
+
}
|
|
234
|
+
function isMp3Format(format) {
|
|
235
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
236
|
+
}
|
|
237
|
+
function isWavFormat(format) {
|
|
238
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
239
|
+
}
|
|
240
|
+
function isRawFormat(format) {
|
|
241
|
+
return /^raw(?:-|$)/i.test(format);
|
|
242
|
+
}
|
|
243
|
+
function resolveMergeAudioFormat(format) {
|
|
244
|
+
if (isWavFormat(format)) return "wav";
|
|
245
|
+
if (isMp3Format(format)) return "mp3";
|
|
246
|
+
if (isRawFormat(format)) return "raw";
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
249
|
+
function canMergeAudioFormat(format) {
|
|
250
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
251
|
+
}
|
|
252
|
+
function mergeAudioBuffers(buffers, format) {
|
|
253
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
254
|
+
if (isMp3Format(format)) {
|
|
255
|
+
const parts = buffers.map(stripMp3Tags);
|
|
256
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
257
|
+
let offset = 0;
|
|
258
|
+
for (const part of parts) {
|
|
259
|
+
output.set(part, offset);
|
|
260
|
+
offset += part.byteLength;
|
|
261
|
+
}
|
|
262
|
+
return output.buffer;
|
|
263
|
+
}
|
|
264
|
+
if (isRawFormat(format)) {
|
|
265
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
266
|
+
let offset = 0;
|
|
267
|
+
for (const buffer of buffers) {
|
|
268
|
+
output.set(new Uint8Array(buffer), offset);
|
|
269
|
+
offset += buffer.byteLength;
|
|
270
|
+
}
|
|
271
|
+
return output.buffer;
|
|
272
|
+
}
|
|
273
|
+
throw new UnsupportedMergeFormatError(format);
|
|
274
|
+
}
|
|
121
275
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
122
276
|
try {
|
|
123
277
|
synthesizer.close();
|
|
@@ -190,12 +344,26 @@ async function synthesizeSsml(ssml, config) {
|
|
|
190
344
|
...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
|
|
191
345
|
);
|
|
192
346
|
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
347
|
+
const requestId = result.resultId;
|
|
348
|
+
const addSourceMetadata = (event) => ({
|
|
349
|
+
...event,
|
|
350
|
+
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
351
|
+
...config.sourceTextRange ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
352
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
353
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
354
|
+
...requestId ? { requestId } : {}
|
|
355
|
+
});
|
|
356
|
+
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
357
|
+
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
358
|
+
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
193
359
|
resolve({
|
|
194
360
|
audioData: result.audioData,
|
|
195
361
|
durationMs,
|
|
196
|
-
...
|
|
197
|
-
...
|
|
198
|
-
...
|
|
362
|
+
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
363
|
+
...requestId ? { requestId } : {},
|
|
364
|
+
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
365
|
+
...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
|
|
366
|
+
...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
|
|
199
367
|
});
|
|
200
368
|
};
|
|
201
369
|
try {
|
|
@@ -215,10 +383,272 @@ async function synthesizeSsml(ssml, config) {
|
|
|
215
383
|
}
|
|
216
384
|
});
|
|
217
385
|
}
|
|
386
|
+
async function synthesizeSsmlChunks(chunks, config) {
|
|
387
|
+
const results = [];
|
|
388
|
+
const totalChunks = chunks.length;
|
|
389
|
+
const report = (event) => config.onProgress?.(event);
|
|
390
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
391
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
392
|
+
report({
|
|
393
|
+
currentChunk: index,
|
|
394
|
+
totalChunks,
|
|
395
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
396
|
+
chunkIndex: index,
|
|
397
|
+
originalTextRange: input.originalTextRange,
|
|
398
|
+
status: "pending",
|
|
399
|
+
durationMs: 0
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
403
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
404
|
+
report({
|
|
405
|
+
currentChunk: index,
|
|
406
|
+
totalChunks,
|
|
407
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
408
|
+
chunkIndex: index,
|
|
409
|
+
originalTextRange: input.originalTextRange,
|
|
410
|
+
status: "synthesizing",
|
|
411
|
+
durationMs: 0
|
|
412
|
+
});
|
|
413
|
+
const startedAt = Date.now();
|
|
414
|
+
try {
|
|
415
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
416
|
+
...config,
|
|
417
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
418
|
+
...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
|
|
419
|
+
chunkIndex: index,
|
|
420
|
+
onProgress: void 0
|
|
421
|
+
});
|
|
422
|
+
results.push(result);
|
|
423
|
+
report({
|
|
424
|
+
currentChunk: index + 1,
|
|
425
|
+
totalChunks,
|
|
426
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
427
|
+
chunkIndex: index,
|
|
428
|
+
originalTextRange: input.originalTextRange,
|
|
429
|
+
status: "success",
|
|
430
|
+
durationMs: Date.now() - startedAt
|
|
431
|
+
});
|
|
432
|
+
} catch (error) {
|
|
433
|
+
report({
|
|
434
|
+
currentChunk: index,
|
|
435
|
+
totalChunks,
|
|
436
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
437
|
+
chunkIndex: index,
|
|
438
|
+
originalTextRange: input.originalTextRange,
|
|
439
|
+
status: "failed",
|
|
440
|
+
durationMs: Date.now() - startedAt,
|
|
441
|
+
error
|
|
442
|
+
});
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
|
|
447
|
+
}
|
|
448
|
+
function mergeSynthesisResults(results, format) {
|
|
449
|
+
const audioData = format ? new Uint8Array(
|
|
450
|
+
mergeAudioBuffers(
|
|
451
|
+
results.map((result) => result.audioData),
|
|
452
|
+
format
|
|
453
|
+
)
|
|
454
|
+
) : new Uint8Array(results.reduce((total, result) => total + result.audioData.byteLength, 0));
|
|
455
|
+
if (!format) {
|
|
456
|
+
let offset = 0;
|
|
457
|
+
for (const result of results) {
|
|
458
|
+
audioData.set(new Uint8Array(result.audioData), offset);
|
|
459
|
+
offset += result.audioData.byteLength;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const boundaries = [];
|
|
463
|
+
const visemes = [];
|
|
464
|
+
const bookmarks = [];
|
|
465
|
+
let durationOffset = 0;
|
|
466
|
+
for (const result of results) {
|
|
467
|
+
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
468
|
+
for (const boundary of chunkBoundaries) {
|
|
469
|
+
const textRange = boundary.textRange ?? result.textRange;
|
|
470
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
471
|
+
const requestId = boundary.requestId ?? result.requestId;
|
|
472
|
+
boundaries.push({
|
|
473
|
+
...boundary,
|
|
474
|
+
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
475
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
476
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
477
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
478
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
479
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
480
|
+
...requestId ? { requestId } : {}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
for (const viseme of result.visemes ?? []) {
|
|
484
|
+
const textRange = viseme.textRange ?? result.textRange;
|
|
485
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
486
|
+
const requestId = viseme.requestId ?? result.requestId;
|
|
487
|
+
visemes.push({
|
|
488
|
+
...viseme,
|
|
489
|
+
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
490
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
491
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
492
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
493
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
494
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
495
|
+
...requestId ? { requestId } : {}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
for (const bookmark of result.bookmarks ?? []) {
|
|
499
|
+
const textRange = bookmark.textRange ?? result.textRange;
|
|
500
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
501
|
+
const requestId = bookmark.requestId ?? result.requestId;
|
|
502
|
+
bookmarks.push({
|
|
503
|
+
...bookmark,
|
|
504
|
+
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
505
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
506
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: results.indexOf(result) } : {},
|
|
507
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
508
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
509
|
+
...textRange ? { textRange: { ...textRange } } : {},
|
|
510
|
+
...requestId ? { requestId } : {}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
durationOffset += Math.max(0, result.durationMs);
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
audioData: audioData.buffer,
|
|
517
|
+
durationMs: durationOffset,
|
|
518
|
+
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
519
|
+
...visemes.length > 0 ? { visemes } : {},
|
|
520
|
+
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
521
|
+
...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
|
|
522
|
+
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
218
525
|
async function synthesizeSpeech(ssml, config) {
|
|
219
526
|
return (await synthesizeSsml(ssml, config)).audioData;
|
|
220
527
|
}
|
|
221
528
|
|
|
529
|
+
// packages/azure-tts-client/src/safe.ts
|
|
530
|
+
var ChunkValidationError = class extends Error {
|
|
531
|
+
constructor(chunkIndex, diagnostics) {
|
|
532
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
533
|
+
this.kind = "chunk-validation";
|
|
534
|
+
this.name = "ChunkValidationError";
|
|
535
|
+
this.chunkIndex = chunkIndex;
|
|
536
|
+
this.diagnostics = diagnostics;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
540
|
+
const validationOptions = options.validation ?? options;
|
|
541
|
+
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
542
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
543
|
+
if (errors.length > 0) {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
success: false,
|
|
547
|
+
status: "validation-error",
|
|
548
|
+
error: {
|
|
549
|
+
kind: "validation",
|
|
550
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
551
|
+
diagnostics: errors
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
return { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
|
|
559
|
+
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
563
|
+
const validationOptions = options.validation ?? options;
|
|
564
|
+
const pending = (index, status, error) => {
|
|
565
|
+
options.onProgress?.({
|
|
566
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
567
|
+
totalChunks: chunks.length,
|
|
568
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
569
|
+
chunkIndex: index,
|
|
570
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
571
|
+
status,
|
|
572
|
+
durationMs: 0,
|
|
573
|
+
...error ? { error } : {}
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
chunks.forEach((_chunk, index) => {
|
|
577
|
+
pending(index, "pending");
|
|
578
|
+
});
|
|
579
|
+
const validations = await Promise.all(
|
|
580
|
+
chunks.map(async (chunk) => {
|
|
581
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
582
|
+
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
583
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
584
|
+
})
|
|
585
|
+
);
|
|
586
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
587
|
+
if (firstInvalidIndex >= 0) {
|
|
588
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
589
|
+
pending(firstInvalidIndex, "failed", error);
|
|
590
|
+
return { ok: false, success: false, status: "validation-error", error };
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
if (client.synthesizeChunks) {
|
|
594
|
+
const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
|
|
595
|
+
return { ok: true, success: true, status: "success", value };
|
|
596
|
+
}
|
|
597
|
+
const results = [];
|
|
598
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
599
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
600
|
+
const sourceNodePath = input.sourceNodePath;
|
|
601
|
+
pending(index, "synthesizing");
|
|
602
|
+
const startedAt = Date.now();
|
|
603
|
+
try {
|
|
604
|
+
const result = await client.synthesizeSsml(input.ssml);
|
|
605
|
+
results.push({
|
|
606
|
+
...result,
|
|
607
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
608
|
+
...sourceNodePath ? {
|
|
609
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
610
|
+
...event,
|
|
611
|
+
sourceNodePath: [...sourceNodePath]
|
|
612
|
+
})),
|
|
613
|
+
visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
614
|
+
bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] }))
|
|
615
|
+
} : {}
|
|
616
|
+
});
|
|
617
|
+
options.onProgress?.({
|
|
618
|
+
currentChunk: index + 1,
|
|
619
|
+
totalChunks: chunks.length,
|
|
620
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
621
|
+
chunkIndex: index,
|
|
622
|
+
originalTextRange: input.originalTextRange,
|
|
623
|
+
status: "success",
|
|
624
|
+
durationMs: Date.now() - startedAt
|
|
625
|
+
});
|
|
626
|
+
} catch (error) {
|
|
627
|
+
options.onProgress?.({
|
|
628
|
+
currentChunk: index,
|
|
629
|
+
totalChunks: chunks.length,
|
|
630
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
631
|
+
chunkIndex: index,
|
|
632
|
+
originalTextRange: input.originalTextRange,
|
|
633
|
+
status: "failed",
|
|
634
|
+
durationMs: Date.now() - startedAt,
|
|
635
|
+
error
|
|
636
|
+
});
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
ok: true,
|
|
642
|
+
success: true,
|
|
643
|
+
status: "success",
|
|
644
|
+
value: mergeSynthesisResults(results, options.outputFormat)
|
|
645
|
+
};
|
|
646
|
+
} catch (error) {
|
|
647
|
+
const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
|
|
648
|
+
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
222
652
|
// packages/azure-tts-client/src/client.ts
|
|
223
653
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
224
654
|
var _options;
|
|
@@ -240,6 +670,32 @@ var AzureTtsClient = class {
|
|
|
240
670
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
241
671
|
return synthesizeSsml(ssml, { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs });
|
|
242
672
|
}
|
|
673
|
+
async synthesizeChunks(chunks, options = {}) {
|
|
674
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
675
|
+
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
676
|
+
return synthesizeSsmlChunks(chunks, {
|
|
677
|
+
endpoint,
|
|
678
|
+
region,
|
|
679
|
+
subscriptionKey,
|
|
680
|
+
outputFormat,
|
|
681
|
+
signal,
|
|
682
|
+
timeoutMs,
|
|
683
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
687
|
+
return synthesizeSsmlSafe(this, ssml, options);
|
|
688
|
+
}
|
|
689
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
690
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
691
|
+
...options,
|
|
692
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
693
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
697
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
698
|
+
}
|
|
243
699
|
};
|
|
244
700
|
_options = new WeakMap();
|
|
245
701
|
|
|
@@ -295,6 +751,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
295
751
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
296
752
|
const styles = stringList(record.StyleList);
|
|
297
753
|
const status = normalizeStatus(record.Status);
|
|
754
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
755
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
756
|
+
const models = stringList(record.Models);
|
|
298
757
|
const merged = {
|
|
299
758
|
name: existing?.name ?? name,
|
|
300
759
|
locale: existing?.locale ?? locale,
|
|
@@ -304,6 +763,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
304
763
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
305
764
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
306
765
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
766
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
767
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
768
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
769
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
770
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
771
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
307
772
|
if (status) merged.status = status;
|
|
308
773
|
else if (existing?.status) merged.status = existing.status;
|
|
309
774
|
voices.set(key, merged);
|
|
@@ -324,9 +789,13 @@ export {
|
|
|
324
789
|
AzureTtsClient,
|
|
325
790
|
AzureTtsError,
|
|
326
791
|
AzureTtsSdkError,
|
|
792
|
+
ChunkValidationError,
|
|
793
|
+
UnsupportedMergeFormatError,
|
|
327
794
|
areAzureLanguagesEquivalent,
|
|
328
795
|
buildPartialSsml,
|
|
329
796
|
buildSsml,
|
|
797
|
+
canMergeAudioFormat,
|
|
798
|
+
createAzureUrlValidatorRunner,
|
|
330
799
|
extractSsmlText,
|
|
331
800
|
extractSsmlTranslatableText,
|
|
332
801
|
fetchAzureVoiceCatalog,
|
|
@@ -335,11 +804,17 @@ export {
|
|
|
335
804
|
getBuiltInVoiceCatalogMetadata,
|
|
336
805
|
isValidAzureAudioDuration,
|
|
337
806
|
mapSsmlTextNodes,
|
|
807
|
+
mergeAudioBuffers,
|
|
808
|
+
mergeSynthesisResults,
|
|
338
809
|
normalizeAzureLanguage,
|
|
339
810
|
parseSsml,
|
|
811
|
+
resolveMergeAudioFormat,
|
|
340
812
|
splitSsmlDocument,
|
|
341
813
|
synthesizeSpeech,
|
|
342
814
|
synthesizeSsml,
|
|
815
|
+
synthesizeSsmlChunks,
|
|
816
|
+
synthesizeSsmlChunksSafe,
|
|
817
|
+
synthesizeSsmlSafe,
|
|
343
818
|
validateAzureSsml,
|
|
344
819
|
validateSsml,
|
|
345
820
|
validateSsmlStructureIntegrity
|