ssml-builder-js 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/README.md +8 -0
- package/dist/{chunk-25LOR4AJ.mjs → chunk-AQ55MOPU.mjs} +115 -14
- package/dist/chunk-AQ55MOPU.mjs.map +1 -0
- package/dist/{chunk-CZ2F3TET.mjs → chunk-QFIBPCO4.mjs} +80 -9
- package/dist/chunk-QFIBPCO4.mjs.map +1 -0
- package/dist/{chunk-LCTE26LS.mjs → chunk-UZSCXPC5.mjs} +2 -2
- package/dist/core.d.mts +18 -1
- package/dist/core.d.ts +18 -1
- package/dist/core.js +115 -13
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +79 -8
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-Xbr6ZWnK.d.mts → index.d-CUXRwSw0.d.mts} +19 -1
- package/dist/{index.d-Xbr6ZWnK.d.ts → index.d-CUXRwSw0.d.ts} +19 -1
- package/dist/index.d.mts +66 -10
- package/dist/index.d.ts +66 -10
- package/dist/index.js +557 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -17
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +5 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.js +106 -12
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +29 -6
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-25LOR4AJ.mjs.map +0 -1
- package/dist/chunk-CZ2F3TET.mjs.map +0 -1
- /package/dist/{chunk-LCTE26LS.mjs.map → chunk-UZSCXPC5.mjs.map} +0 -0
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,10 +16,10 @@ import {
|
|
|
15
16
|
validateAzureSsml,
|
|
16
17
|
validateSsml,
|
|
17
18
|
validateSsmlStructureIntegrity
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-AQ55MOPU.mjs";
|
|
19
20
|
import {
|
|
20
21
|
validateAzureSsml as validateAzureSsml2
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-QFIBPCO4.mjs";
|
|
22
23
|
import {
|
|
23
24
|
__privateAdd,
|
|
24
25
|
__privateGet,
|
|
@@ -44,6 +45,13 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
44
45
|
this.errorDetails = errorDetails;
|
|
45
46
|
}
|
|
46
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
|
+
};
|
|
47
55
|
function createSpeechSdkError(error) {
|
|
48
56
|
const message = error instanceof Error ? error.message : String(error);
|
|
49
57
|
return new AzureTtsSdkError(message);
|
|
@@ -121,6 +129,149 @@ function createSpeechConfig(config) {
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
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
|
+
}
|
|
124
275
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
125
276
|
try {
|
|
126
277
|
synthesizer.close();
|
|
@@ -197,6 +348,9 @@ async function synthesizeSsml(ssml, config) {
|
|
|
197
348
|
const addSourceMetadata = (event) => ({
|
|
198
349
|
...event,
|
|
199
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] } : {},
|
|
200
354
|
...requestId ? { requestId } : {}
|
|
201
355
|
});
|
|
202
356
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -232,60 +386,126 @@ async function synthesizeSsml(ssml, config) {
|
|
|
232
386
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
233
387
|
const results = [];
|
|
234
388
|
const totalChunks = chunks.length;
|
|
389
|
+
const report = (event) => config.onProgress?.(event);
|
|
235
390
|
for (const [index, chunk] of chunks.entries()) {
|
|
236
391
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
241
400
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
401
|
+
}
|
|
402
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
403
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
404
|
+
report({
|
|
405
|
+
currentChunk: index,
|
|
245
406
|
totalChunks,
|
|
246
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
407
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
408
|
+
chunkIndex: index,
|
|
409
|
+
originalTextRange: input.originalTextRange,
|
|
410
|
+
status: "synthesizing",
|
|
411
|
+
durationMs: 0
|
|
247
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
|
+
}
|
|
248
445
|
}
|
|
249
|
-
return mergeSynthesisResults(results);
|
|
446
|
+
return mergeSynthesisResults(results, config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3");
|
|
250
447
|
}
|
|
251
|
-
function mergeSynthesisResults(results) {
|
|
252
|
-
const
|
|
253
|
-
|
|
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
|
+
}
|
|
254
462
|
const boundaries = [];
|
|
255
463
|
const visemes = [];
|
|
256
464
|
const bookmarks = [];
|
|
257
|
-
let byteOffset = 0;
|
|
258
465
|
let durationOffset = 0;
|
|
259
466
|
for (const result of results) {
|
|
260
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
261
|
-
byteOffset += result.audioData.byteLength;
|
|
262
467
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
263
468
|
for (const boundary of chunkBoundaries) {
|
|
264
469
|
const textRange = boundary.textRange ?? result.textRange;
|
|
470
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
265
471
|
const requestId = boundary.requestId ?? result.requestId;
|
|
266
472
|
boundaries.push({
|
|
267
473
|
...boundary,
|
|
268
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 } } : {},
|
|
269
479
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
270
480
|
...requestId ? { requestId } : {}
|
|
271
481
|
});
|
|
272
482
|
}
|
|
273
483
|
for (const viseme of result.visemes ?? []) {
|
|
274
484
|
const textRange = viseme.textRange ?? result.textRange;
|
|
485
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
275
486
|
const requestId = viseme.requestId ?? result.requestId;
|
|
276
487
|
visemes.push({
|
|
277
488
|
...viseme,
|
|
278
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 } } : {},
|
|
279
494
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
280
495
|
...requestId ? { requestId } : {}
|
|
281
496
|
});
|
|
282
497
|
}
|
|
283
498
|
for (const bookmark of result.bookmarks ?? []) {
|
|
284
499
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
500
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
285
501
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
286
502
|
bookmarks.push({
|
|
287
503
|
...bookmark,
|
|
288
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 } } : {},
|
|
289
509
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
290
510
|
...requestId ? { requestId } : {}
|
|
291
511
|
});
|
|
@@ -307,6 +527,15 @@ async function synthesizeSpeech(ssml, config) {
|
|
|
307
527
|
}
|
|
308
528
|
|
|
309
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
|
+
};
|
|
310
539
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
311
540
|
const validationOptions = options.validation ?? options;
|
|
312
541
|
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
@@ -330,6 +559,95 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
330
559
|
return { ok: false, success: false, status: "azure-api-error", error: azureError };
|
|
331
560
|
}
|
|
332
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
|
+
}
|
|
333
651
|
|
|
334
652
|
// packages/azure-tts-client/src/client.ts
|
|
335
653
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -368,6 +686,16 @@ var AzureTtsClient = class {
|
|
|
368
686
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
369
687
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
370
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
|
+
}
|
|
371
699
|
};
|
|
372
700
|
_options = new WeakMap();
|
|
373
701
|
|
|
@@ -423,6 +751,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
423
751
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
424
752
|
const styles = stringList(record.StyleList);
|
|
425
753
|
const status = normalizeStatus(record.Status);
|
|
754
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
755
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
756
|
+
const models = stringList(record.Models);
|
|
426
757
|
const merged = {
|
|
427
758
|
name: existing?.name ?? name,
|
|
428
759
|
locale: existing?.locale ?? locale,
|
|
@@ -432,6 +763,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
432
763
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
433
764
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
434
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;
|
|
435
772
|
if (status) merged.status = status;
|
|
436
773
|
else if (existing?.status) merged.status = existing.status;
|
|
437
774
|
voices.set(key, merged);
|
|
@@ -452,9 +789,13 @@ export {
|
|
|
452
789
|
AzureTtsClient,
|
|
453
790
|
AzureTtsError,
|
|
454
791
|
AzureTtsSdkError,
|
|
792
|
+
ChunkValidationError,
|
|
793
|
+
UnsupportedMergeFormatError,
|
|
455
794
|
areAzureLanguagesEquivalent,
|
|
456
795
|
buildPartialSsml,
|
|
457
796
|
buildSsml,
|
|
797
|
+
canMergeAudioFormat,
|
|
798
|
+
createAzureUrlValidatorRunner,
|
|
458
799
|
extractSsmlText,
|
|
459
800
|
extractSsmlTranslatableText,
|
|
460
801
|
fetchAzureVoiceCatalog,
|
|
@@ -463,13 +804,16 @@ export {
|
|
|
463
804
|
getBuiltInVoiceCatalogMetadata,
|
|
464
805
|
isValidAzureAudioDuration,
|
|
465
806
|
mapSsmlTextNodes,
|
|
807
|
+
mergeAudioBuffers,
|
|
466
808
|
mergeSynthesisResults,
|
|
467
809
|
normalizeAzureLanguage,
|
|
468
810
|
parseSsml,
|
|
811
|
+
resolveMergeAudioFormat,
|
|
469
812
|
splitSsmlDocument,
|
|
470
813
|
synthesizeSpeech,
|
|
471
814
|
synthesizeSsml,
|
|
472
815
|
synthesizeSsmlChunks,
|
|
816
|
+
synthesizeSsmlChunksSafe,
|
|
473
817
|
synthesizeSsmlSafe,
|
|
474
818
|
validateAzureSsml,
|
|
475
819
|
validateSsml,
|