@ssml-builder-js/azure-tts-client 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.d.mts +169 -17
- package/dist/index.d.ts +169 -17
- package/dist/index.js +619 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +606 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +39 -7
- package/src/errors.ts +74 -0
- package/src/index.ts +25 -3
- package/src/outputFormats.ts +14 -3
- package/src/safe.ts +273 -23
- package/src/synthesis.ts +435 -26
- package/src/types.ts +48 -2
- package/src/voiceCatalog.ts +15 -0
- package/test/synthesis.test.ts +64 -0
- package/test/v213-pipeline.test.ts +27 -16
- package/test/v214-pipeline.test.ts +114 -0
- package/test/v215-pipeline.test.ts +104 -0
package/dist/index.js
CHANGED
|
@@ -40,11 +40,22 @@ __export(index_exports, {
|
|
|
40
40
|
AzureTtsClient: () => AzureTtsClient,
|
|
41
41
|
AzureTtsError: () => AzureTtsError,
|
|
42
42
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
43
|
+
ChunkValidationError: () => ChunkValidationError,
|
|
44
|
+
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
45
|
+
MergeError: () => MergeError,
|
|
46
|
+
SynthesisCancelledError: () => SynthesisCancelledError,
|
|
47
|
+
SynthesisTimeoutError: () => SynthesisTimeoutError,
|
|
48
|
+
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
49
|
+
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
43
50
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
51
|
+
mergeAudioBuffers: () => mergeAudioBuffers,
|
|
44
52
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
53
|
+
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
54
|
+
resolveMimeType: () => resolveMimeType,
|
|
45
55
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
46
56
|
synthesizeSsml: () => synthesizeSsml,
|
|
47
57
|
synthesizeSsmlChunks: () => synthesizeSsmlChunks,
|
|
58
|
+
synthesizeSsmlChunksSafe: () => synthesizeSsmlChunksSafe,
|
|
48
59
|
synthesizeSsmlSafe: () => synthesizeSsmlSafe
|
|
49
60
|
});
|
|
50
61
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -53,6 +64,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
53
64
|
var AzureTtsError = class extends Error {
|
|
54
65
|
constructor(status, statusText, responseBody, requestId) {
|
|
55
66
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
67
|
+
this.kind = "azure-api-error";
|
|
56
68
|
this.name = "AzureTtsError";
|
|
57
69
|
this.status = status;
|
|
58
70
|
this.statusText = statusText;
|
|
@@ -68,6 +80,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
68
80
|
this.errorDetails = errorDetails;
|
|
69
81
|
}
|
|
70
82
|
};
|
|
83
|
+
var SynthesisCancelledError = class extends Error {
|
|
84
|
+
constructor(message = "Speech synthesis was cancelled.") {
|
|
85
|
+
super(message);
|
|
86
|
+
this.kind = "cancelled";
|
|
87
|
+
this.name = "SynthesisCancelledError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var SynthesisTimeoutError = class extends Error {
|
|
91
|
+
constructor(message) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.kind = "timeout";
|
|
94
|
+
this.name = "SynthesisTimeoutError";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var MergeError = class extends Error {
|
|
98
|
+
constructor(message, cause) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.kind = "merge-error";
|
|
101
|
+
this.name = "MergeError";
|
|
102
|
+
this.cause = cause;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
106
|
+
constructor(format) {
|
|
107
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
108
|
+
this.kind = "unsupported-format-error";
|
|
109
|
+
this.name = "UnsupportedMergeFormatError";
|
|
110
|
+
this.format = format;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function toSynthesisError(error) {
|
|
114
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
115
|
+
return error;
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
118
|
+
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
119
|
+
return createSpeechSdkError(error);
|
|
120
|
+
}
|
|
71
121
|
function createSpeechSdkError(error) {
|
|
72
122
|
const message = error instanceof Error ? error.message : String(error);
|
|
73
123
|
return new AzureTtsSdkError(message);
|
|
@@ -75,9 +125,7 @@ function createSpeechSdkError(error) {
|
|
|
75
125
|
|
|
76
126
|
// src/synthesis.ts
|
|
77
127
|
var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
78
|
-
|
|
79
|
-
// src/speechConfig.ts
|
|
80
|
-
var import_microsoft_cognitiveservices_speech_sdk = require("microsoft-cognitiveservices-speech-sdk");
|
|
128
|
+
var import_ssml_core = require("@ssml-builder-js/ssml-core");
|
|
81
129
|
|
|
82
130
|
// src/outputFormats.ts
|
|
83
131
|
var SpeechSDK = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
@@ -123,6 +171,14 @@ var OUTPUT_FORMATS = {
|
|
|
123
171
|
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
124
172
|
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
125
173
|
};
|
|
174
|
+
function resolveMimeType(outputFormat) {
|
|
175
|
+
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
176
|
+
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
177
|
+
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
178
|
+
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
179
|
+
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
180
|
+
return "application/octet-stream";
|
|
181
|
+
}
|
|
126
182
|
function resolveOutputFormat(outputFormat) {
|
|
127
183
|
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
128
184
|
if (resolvedFormat === void 0) {
|
|
@@ -132,6 +188,7 @@ function resolveOutputFormat(outputFormat) {
|
|
|
132
188
|
}
|
|
133
189
|
|
|
134
190
|
// src/speechConfig.ts
|
|
191
|
+
var import_microsoft_cognitiveservices_speech_sdk = require("microsoft-cognitiveservices-speech-sdk");
|
|
135
192
|
function resolveEndpoint(config) {
|
|
136
193
|
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
137
194
|
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
@@ -145,6 +202,156 @@ function createSpeechConfig(config) {
|
|
|
145
202
|
}
|
|
146
203
|
|
|
147
204
|
// src/synthesis.ts
|
|
205
|
+
function ascii(bytes, offset, value) {
|
|
206
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
207
|
+
}
|
|
208
|
+
function readUint32(bytes, offset) {
|
|
209
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
210
|
+
}
|
|
211
|
+
function parseWav(buffer) {
|
|
212
|
+
const bytes = new Uint8Array(buffer);
|
|
213
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
214
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
215
|
+
}
|
|
216
|
+
const chunks = [];
|
|
217
|
+
const dataParts = [];
|
|
218
|
+
let format;
|
|
219
|
+
let offset = 12;
|
|
220
|
+
while (offset < bytes.byteLength) {
|
|
221
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
222
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
223
|
+
const size = readUint32(bytes, offset + 4);
|
|
224
|
+
const dataStart = offset + 8;
|
|
225
|
+
const dataEnd = dataStart + size;
|
|
226
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
227
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
228
|
+
chunks.push({ id, data: data2 });
|
|
229
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
230
|
+
if (id === "data") dataParts.push(data2);
|
|
231
|
+
offset = dataEnd + (size & 1);
|
|
232
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
233
|
+
}
|
|
234
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
235
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
236
|
+
const data = new Uint8Array(dataLength);
|
|
237
|
+
let dataOffset = 0;
|
|
238
|
+
for (const part of dataParts) {
|
|
239
|
+
data.set(part, dataOffset);
|
|
240
|
+
dataOffset += part.byteLength;
|
|
241
|
+
}
|
|
242
|
+
return { chunks, data, format };
|
|
243
|
+
}
|
|
244
|
+
function writeUint32(target, offset, value) {
|
|
245
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
246
|
+
}
|
|
247
|
+
function writeChunk(target, offset, id, data) {
|
|
248
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
249
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
250
|
+
target.set(data, offset + 8);
|
|
251
|
+
const end = offset + 8 + data.byteLength;
|
|
252
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
253
|
+
return end + (data.byteLength & 1);
|
|
254
|
+
}
|
|
255
|
+
function mergeWavBuffers(buffers) {
|
|
256
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
257
|
+
const parsed = buffers.map(parseWav);
|
|
258
|
+
const first = parsed[0];
|
|
259
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
260
|
+
if (parsed.some(
|
|
261
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
262
|
+
))
|
|
263
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
264
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
265
|
+
const nonDataLength = first.chunks.reduce(
|
|
266
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
267
|
+
0
|
|
268
|
+
);
|
|
269
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
270
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
271
|
+
const output = new Uint8Array(outputLength);
|
|
272
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
273
|
+
writeUint32(output, 4, outputLength - 8);
|
|
274
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
275
|
+
let outputOffset = 12;
|
|
276
|
+
let dataWritten = false;
|
|
277
|
+
for (const chunk of first.chunks) {
|
|
278
|
+
if (chunk.id === "data") {
|
|
279
|
+
if (dataWritten) continue;
|
|
280
|
+
const data = new Uint8Array(dataLength);
|
|
281
|
+
let dataOffset = 0;
|
|
282
|
+
for (const item of parsed) {
|
|
283
|
+
data.set(item.data, dataOffset);
|
|
284
|
+
dataOffset += item.data.byteLength;
|
|
285
|
+
}
|
|
286
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
287
|
+
dataWritten = true;
|
|
288
|
+
} else {
|
|
289
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
293
|
+
return output.buffer;
|
|
294
|
+
}
|
|
295
|
+
function skipId3v2(bytes) {
|
|
296
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
297
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
298
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
299
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
300
|
+
}
|
|
301
|
+
function stripMp3Tags(buffer) {
|
|
302
|
+
const bytes = new Uint8Array(buffer);
|
|
303
|
+
const start = skipId3v2(bytes);
|
|
304
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
305
|
+
return bytes.slice(Math.min(start, end), end);
|
|
306
|
+
}
|
|
307
|
+
function isMp3Format(format) {
|
|
308
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
309
|
+
}
|
|
310
|
+
function isWavFormat(format) {
|
|
311
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
312
|
+
}
|
|
313
|
+
function isRawFormat(format) {
|
|
314
|
+
return /^raw(?:-|$)/i.test(format);
|
|
315
|
+
}
|
|
316
|
+
function resolveMergeAudioFormat(format) {
|
|
317
|
+
if (isWavFormat(format)) return "wav";
|
|
318
|
+
if (isMp3Format(format)) return "mp3";
|
|
319
|
+
if (isRawFormat(format)) return "raw";
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
function canMergeAudioFormat(format) {
|
|
323
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
324
|
+
}
|
|
325
|
+
function mergeAudioBuffers(buffers, options) {
|
|
326
|
+
const format = typeof options === "string" ? options : options?.format;
|
|
327
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
328
|
+
try {
|
|
329
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
330
|
+
if (isMp3Format(format)) {
|
|
331
|
+
const parts = buffers.map(stripMp3Tags);
|
|
332
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
333
|
+
let offset = 0;
|
|
334
|
+
for (const part of parts) {
|
|
335
|
+
output.set(part, offset);
|
|
336
|
+
offset += part.byteLength;
|
|
337
|
+
}
|
|
338
|
+
return output.buffer;
|
|
339
|
+
}
|
|
340
|
+
if (isRawFormat(format)) {
|
|
341
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
342
|
+
let offset = 0;
|
|
343
|
+
for (const buffer of buffers) {
|
|
344
|
+
output.set(new Uint8Array(buffer), offset);
|
|
345
|
+
offset += buffer.byteLength;
|
|
346
|
+
}
|
|
347
|
+
return output.buffer;
|
|
348
|
+
}
|
|
349
|
+
throw new UnsupportedMergeFormatError(format);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
352
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
148
355
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
149
356
|
try {
|
|
150
357
|
synthesizer.close();
|
|
@@ -158,7 +365,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
158
365
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
159
366
|
async function synthesizeSsml(ssml, config) {
|
|
160
367
|
if (config.signal?.aborted) {
|
|
161
|
-
throw
|
|
368
|
+
throw new SynthesisCancelledError();
|
|
162
369
|
}
|
|
163
370
|
const speechConfig = createSpeechConfig(config);
|
|
164
371
|
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
@@ -181,23 +388,94 @@ async function synthesizeSsml(ssml, config) {
|
|
|
181
388
|
settled = true;
|
|
182
389
|
cleanup();
|
|
183
390
|
closeResources();
|
|
184
|
-
reject(
|
|
391
|
+
reject(toSynthesisError(error));
|
|
185
392
|
};
|
|
186
393
|
const boundaries = [];
|
|
187
394
|
const visemes = [];
|
|
188
395
|
const bookmarks = [];
|
|
396
|
+
let sourceEventCursor = 0;
|
|
397
|
+
let generatedSourceMap;
|
|
398
|
+
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
399
|
+
try {
|
|
400
|
+
generatedSourceMap = (0, import_ssml_core.getSsmlSourceMap)(ssml);
|
|
401
|
+
} catch {
|
|
402
|
+
generatedSourceMap = void 0;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
406
|
+
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
407
|
+
...segment,
|
|
408
|
+
range: {
|
|
409
|
+
start: segment.range.start + sourceBaseOffset,
|
|
410
|
+
end: segment.range.end + sourceBaseOffset
|
|
411
|
+
},
|
|
412
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
413
|
+
})) ?? [];
|
|
414
|
+
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
415
|
+
...marker,
|
|
416
|
+
originalTextRange: {
|
|
417
|
+
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
418
|
+
end: marker.originalTextRange.end + sourceBaseOffset
|
|
419
|
+
},
|
|
420
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
421
|
+
})) ?? [];
|
|
422
|
+
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
423
|
+
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
424
|
+
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
425
|
+
if (marker) {
|
|
426
|
+
return {
|
|
427
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
428
|
+
sourceNodePath: [...marker.sourceNodePath],
|
|
429
|
+
textRange: { ...marker.originalTextRange }
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
|
|
433
|
+
const value = text ?? "";
|
|
434
|
+
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
435
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
|
|
436
|
+
localStart = -1;
|
|
437
|
+
if (localStart < 0 || localStart > sourceText.length) {
|
|
438
|
+
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
439
|
+
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
440
|
+
}
|
|
441
|
+
localStart = Math.max(0, localStart);
|
|
442
|
+
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
443
|
+
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
444
|
+
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
445
|
+
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
446
|
+
const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
|
|
447
|
+
return {
|
|
448
|
+
originalTextRange: { ...fallbackRange },
|
|
449
|
+
textRange: { ...fallbackRange },
|
|
450
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
451
|
+
};
|
|
452
|
+
};
|
|
189
453
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
190
454
|
boundaries.push({
|
|
191
455
|
text: event.text,
|
|
192
456
|
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
193
|
-
durationMs: ticksToMilliseconds(event.duration)
|
|
457
|
+
durationMs: ticksToMilliseconds(event.duration),
|
|
458
|
+
...mapSourceEvent(
|
|
459
|
+
event.text,
|
|
460
|
+
event.textOffset
|
|
461
|
+
)
|
|
194
462
|
});
|
|
195
463
|
};
|
|
196
464
|
synthesizer.visemeReceived = (_sender, event) => {
|
|
197
|
-
|
|
465
|
+
const eventWithOffset = event;
|
|
466
|
+
visemes.push({
|
|
467
|
+
visemeId: event.visemeId,
|
|
468
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
469
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
470
|
+
});
|
|
198
471
|
};
|
|
199
472
|
synthesizer.bookmarkReached = (_sender, event) => {
|
|
200
|
-
|
|
473
|
+
const eventWithOffset = event;
|
|
474
|
+
bookmarks.push({
|
|
475
|
+
name: event.text,
|
|
476
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
477
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
478
|
+
});
|
|
201
479
|
};
|
|
202
480
|
const cb = (result) => {
|
|
203
481
|
if (settled) return;
|
|
@@ -220,7 +498,10 @@ async function synthesizeSsml(ssml, config) {
|
|
|
220
498
|
const requestId = result.resultId;
|
|
221
499
|
const addSourceMetadata = (event) => ({
|
|
222
500
|
...event,
|
|
223
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
501
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
502
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
503
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
504
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
224
505
|
...requestId ? { requestId } : {}
|
|
225
506
|
});
|
|
226
507
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -238,12 +519,12 @@ async function synthesizeSsml(ssml, config) {
|
|
|
238
519
|
};
|
|
239
520
|
try {
|
|
240
521
|
if (config.signal) {
|
|
241
|
-
abortHandler = () => rejectWithError(
|
|
522
|
+
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
242
523
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
243
524
|
}
|
|
244
525
|
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
245
526
|
timeout = setTimeout(
|
|
246
|
-
() => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
|
|
527
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
|
|
247
528
|
config.timeoutMs
|
|
248
529
|
);
|
|
249
530
|
}
|
|
@@ -256,60 +537,117 @@ async function synthesizeSsml(ssml, config) {
|
|
|
256
537
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
257
538
|
const results = [];
|
|
258
539
|
const totalChunks = chunks.length;
|
|
540
|
+
const report = (event) => config.onProgress?.(event);
|
|
259
541
|
for (const [index, chunk] of chunks.entries()) {
|
|
260
542
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
543
|
+
report({
|
|
544
|
+
currentChunk: index,
|
|
545
|
+
totalChunks,
|
|
546
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
547
|
+
chunkIndex: index,
|
|
548
|
+
originalTextRange: input.originalTextRange,
|
|
549
|
+
status: "pending",
|
|
550
|
+
durationMs: 0
|
|
265
551
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
552
|
+
}
|
|
553
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
554
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
555
|
+
report({
|
|
556
|
+
currentChunk: index,
|
|
269
557
|
totalChunks,
|
|
270
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
558
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
559
|
+
chunkIndex: index,
|
|
560
|
+
originalTextRange: input.originalTextRange,
|
|
561
|
+
status: "synthesizing",
|
|
562
|
+
durationMs: 0
|
|
271
563
|
});
|
|
564
|
+
const startedAt = Date.now();
|
|
565
|
+
try {
|
|
566
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
567
|
+
...config,
|
|
568
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
569
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
570
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
571
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
572
|
+
chunkIndex: index,
|
|
573
|
+
onProgress: void 0
|
|
574
|
+
});
|
|
575
|
+
results.push(result);
|
|
576
|
+
report({
|
|
577
|
+
currentChunk: index + 1,
|
|
578
|
+
totalChunks,
|
|
579
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
580
|
+
chunkIndex: index,
|
|
581
|
+
originalTextRange: input.originalTextRange,
|
|
582
|
+
status: "success",
|
|
583
|
+
durationMs: Date.now() - startedAt
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
report({
|
|
587
|
+
currentChunk: index,
|
|
588
|
+
totalChunks,
|
|
589
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
590
|
+
chunkIndex: index,
|
|
591
|
+
originalTextRange: input.originalTextRange,
|
|
592
|
+
status: "failed",
|
|
593
|
+
durationMs: Date.now() - startedAt,
|
|
594
|
+
error
|
|
595
|
+
});
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
272
598
|
}
|
|
273
|
-
return mergeSynthesisResults(results
|
|
599
|
+
return mergeSynthesisResults(results, {
|
|
600
|
+
format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
601
|
+
});
|
|
274
602
|
}
|
|
275
|
-
function
|
|
276
|
-
const audioLength = results.reduce((total, result) => total + result.audioData.byteLength, 0);
|
|
277
|
-
const audioData = new Uint8Array(audioLength);
|
|
603
|
+
function createMergedResult(results, audioData, format) {
|
|
278
604
|
const boundaries = [];
|
|
279
605
|
const visemes = [];
|
|
280
606
|
const bookmarks = [];
|
|
281
|
-
let byteOffset = 0;
|
|
282
607
|
let durationOffset = 0;
|
|
283
|
-
for (const result of results) {
|
|
284
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
285
|
-
byteOffset += result.audioData.byteLength;
|
|
608
|
+
for (const [resultIndex, result] of results.entries()) {
|
|
286
609
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
287
610
|
for (const boundary of chunkBoundaries) {
|
|
288
611
|
const textRange = boundary.textRange ?? result.textRange;
|
|
612
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
289
613
|
const requestId = boundary.requestId ?? result.requestId;
|
|
290
614
|
boundaries.push({
|
|
291
615
|
...boundary,
|
|
292
616
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
617
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
618
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
619
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
620
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
293
621
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
294
622
|
...requestId ? { requestId } : {}
|
|
295
623
|
});
|
|
296
624
|
}
|
|
297
625
|
for (const viseme of result.visemes ?? []) {
|
|
298
626
|
const textRange = viseme.textRange ?? result.textRange;
|
|
627
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
299
628
|
const requestId = viseme.requestId ?? result.requestId;
|
|
300
629
|
visemes.push({
|
|
301
630
|
...viseme,
|
|
302
631
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
632
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
633
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
634
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
635
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
303
636
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
304
637
|
...requestId ? { requestId } : {}
|
|
305
638
|
});
|
|
306
639
|
}
|
|
307
640
|
for (const bookmark of result.bookmarks ?? []) {
|
|
308
641
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
642
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
309
643
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
310
644
|
bookmarks.push({
|
|
311
645
|
...bookmark,
|
|
312
646
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
647
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
648
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
649
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
650
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
313
651
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
314
652
|
...requestId ? { requestId } : {}
|
|
315
653
|
});
|
|
@@ -317,8 +655,9 @@ function mergeSynthesisResults(results) {
|
|
|
317
655
|
durationOffset += Math.max(0, result.durationMs);
|
|
318
656
|
}
|
|
319
657
|
return {
|
|
320
|
-
audioData
|
|
658
|
+
audioData,
|
|
321
659
|
durationMs: durationOffset,
|
|
660
|
+
mimeType: resolveMimeType(format),
|
|
322
661
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
323
662
|
...visemes.length > 0 ? { visemes } : {},
|
|
324
663
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -326,35 +665,225 @@ function mergeSynthesisResults(results) {
|
|
|
326
665
|
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
327
666
|
};
|
|
328
667
|
}
|
|
668
|
+
function mergeSynthesisResults(results, options) {
|
|
669
|
+
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
670
|
+
const format = resolvedOptions?.format;
|
|
671
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
672
|
+
const buffers = results.map((result) => result.audioData);
|
|
673
|
+
if (resolvedOptions.customMerger) {
|
|
674
|
+
return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
|
|
675
|
+
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
676
|
+
return createMergedResult(results, merged, format);
|
|
677
|
+
}).catch((error) => {
|
|
678
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
679
|
+
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
686
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
329
689
|
async function synthesizeSpeech(ssml, config) {
|
|
330
690
|
return (await synthesizeSsml(ssml, config)).audioData;
|
|
331
691
|
}
|
|
332
692
|
|
|
333
693
|
// src/safe.ts
|
|
334
|
-
var
|
|
694
|
+
var import_ssml_core2 = require("@ssml-builder-js/ssml-core");
|
|
695
|
+
var ChunkValidationError = class extends Error {
|
|
696
|
+
constructor(chunkIndex, diagnostics) {
|
|
697
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
698
|
+
this.kind = "validation-error";
|
|
699
|
+
this.name = "ChunkValidationError";
|
|
700
|
+
this.chunkIndex = chunkIndex;
|
|
701
|
+
this.diagnostics = diagnostics;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
function failure(error) {
|
|
705
|
+
return { ok: false, success: false, status: error.kind, error };
|
|
706
|
+
}
|
|
335
707
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
336
|
-
const validationOptions = options.validation ?? options;
|
|
337
|
-
const diagnostics = await Promise.resolve((0,
|
|
708
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
709
|
+
const diagnostics = await Promise.resolve((0, import_ssml_core2.validateAzureSsml)(ssml, validationOptions));
|
|
710
|
+
if (options.signal?.aborted) {
|
|
711
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
712
|
+
return failure(error);
|
|
713
|
+
}
|
|
338
714
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
339
715
|
if (errors.length > 0) {
|
|
716
|
+
return failure({
|
|
717
|
+
kind: "validation-error",
|
|
718
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
719
|
+
diagnostics: errors
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
340
723
|
return {
|
|
341
|
-
ok:
|
|
342
|
-
success:
|
|
343
|
-
status: "
|
|
344
|
-
|
|
345
|
-
kind: "validation",
|
|
346
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
347
|
-
diagnostics: errors
|
|
348
|
-
}
|
|
724
|
+
ok: true,
|
|
725
|
+
success: true,
|
|
726
|
+
status: "success",
|
|
727
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal })
|
|
349
728
|
};
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const synthesisError = toSynthesisError(error);
|
|
731
|
+
return failure(synthesisError);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
735
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
736
|
+
if (options.signal?.aborted) {
|
|
737
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
738
|
+
return failure(error);
|
|
739
|
+
}
|
|
740
|
+
const pending = (index, status, error) => {
|
|
741
|
+
options.onProgress?.({
|
|
742
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
743
|
+
totalChunks: chunks.length,
|
|
744
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
745
|
+
chunkIndex: index,
|
|
746
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
747
|
+
status,
|
|
748
|
+
durationMs: 0,
|
|
749
|
+
...error ? { error } : {}
|
|
750
|
+
});
|
|
751
|
+
};
|
|
752
|
+
chunks.forEach((_chunk, index) => {
|
|
753
|
+
pending(index, "pending");
|
|
754
|
+
});
|
|
755
|
+
const validations = await Promise.all(
|
|
756
|
+
chunks.map(async (chunk) => {
|
|
757
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
758
|
+
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
759
|
+
const diagnostics = await Promise.resolve(
|
|
760
|
+
(0, import_ssml_core2.validateAzureSsml)(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
|
|
761
|
+
);
|
|
762
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
766
|
+
if (firstInvalidIndex >= 0) {
|
|
767
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
768
|
+
pending(firstInvalidIndex, "failed", error);
|
|
769
|
+
return failure(error);
|
|
350
770
|
}
|
|
351
771
|
try {
|
|
352
|
-
|
|
772
|
+
if (client.synthesizeChunks) {
|
|
773
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
774
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
775
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
776
|
+
});
|
|
777
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
778
|
+
onProgress: options.onProgress,
|
|
779
|
+
outputFormat: options.outputFormat,
|
|
780
|
+
signal: options.signal,
|
|
781
|
+
timeoutMs: options.timeoutMs,
|
|
782
|
+
sourceNodePath: options.sourceNodePath
|
|
783
|
+
});
|
|
784
|
+
return { ok: true, success: true, status: "success", value };
|
|
785
|
+
}
|
|
786
|
+
const results = [];
|
|
787
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
788
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
789
|
+
const sourceNodePath = input.sourceNodePath;
|
|
790
|
+
const originalTextRange = input.originalTextRange;
|
|
791
|
+
pending(index, "synthesizing");
|
|
792
|
+
const startedAt = Date.now();
|
|
793
|
+
try {
|
|
794
|
+
const result = await client.synthesizeSsml(input.ssml, {
|
|
795
|
+
outputFormat: options.outputFormat,
|
|
796
|
+
signal: options.signal,
|
|
797
|
+
timeoutMs: options.timeoutMs,
|
|
798
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
799
|
+
});
|
|
800
|
+
results.push({
|
|
801
|
+
...result,
|
|
802
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
803
|
+
...sourceNodePath ? {
|
|
804
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
805
|
+
...event,
|
|
806
|
+
sourceNodePath: [...sourceNodePath],
|
|
807
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
808
|
+
})),
|
|
809
|
+
visemes: result.visemes?.map((event) => ({
|
|
810
|
+
...event,
|
|
811
|
+
sourceNodePath: [...sourceNodePath],
|
|
812
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
813
|
+
})),
|
|
814
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
815
|
+
...event,
|
|
816
|
+
sourceNodePath: [...sourceNodePath],
|
|
817
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
818
|
+
}))
|
|
819
|
+
} : {},
|
|
820
|
+
...originalTextRange ? {
|
|
821
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
822
|
+
...event,
|
|
823
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
824
|
+
})),
|
|
825
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
826
|
+
...event,
|
|
827
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
828
|
+
})),
|
|
829
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
830
|
+
...event,
|
|
831
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
832
|
+
})),
|
|
833
|
+
visemes: result.visemes?.map((event) => ({
|
|
834
|
+
...event,
|
|
835
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
836
|
+
})),
|
|
837
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
838
|
+
...event,
|
|
839
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
840
|
+
}))
|
|
841
|
+
} : {}
|
|
842
|
+
});
|
|
843
|
+
options.onProgress?.({
|
|
844
|
+
currentChunk: index + 1,
|
|
845
|
+
totalChunks: chunks.length,
|
|
846
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
847
|
+
chunkIndex: index,
|
|
848
|
+
originalTextRange: input.originalTextRange,
|
|
849
|
+
status: "success",
|
|
850
|
+
durationMs: Date.now() - startedAt
|
|
851
|
+
});
|
|
852
|
+
} catch (error) {
|
|
853
|
+
options.onProgress?.({
|
|
854
|
+
currentChunk: index,
|
|
855
|
+
totalChunks: chunks.length,
|
|
856
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
857
|
+
chunkIndex: index,
|
|
858
|
+
originalTextRange: input.originalTextRange,
|
|
859
|
+
status: "failed",
|
|
860
|
+
durationMs: Date.now() - startedAt,
|
|
861
|
+
error
|
|
862
|
+
});
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
ok: true,
|
|
868
|
+
success: true,
|
|
869
|
+
status: "success",
|
|
870
|
+
value: mergeSynthesisResults(results, {
|
|
871
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
872
|
+
})
|
|
873
|
+
};
|
|
353
874
|
} catch (error) {
|
|
354
|
-
const
|
|
355
|
-
return
|
|
875
|
+
const synthesisError = toSynthesisError(error);
|
|
876
|
+
return failure(synthesisError);
|
|
356
877
|
}
|
|
357
878
|
}
|
|
879
|
+
function withValidationSignal(options, signal) {
|
|
880
|
+
if (!signal) return options;
|
|
881
|
+
return {
|
|
882
|
+
...options,
|
|
883
|
+
urlValidatorSignal: signal,
|
|
884
|
+
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
885
|
+
};
|
|
886
|
+
}
|
|
358
887
|
|
|
359
888
|
// src/client.ts
|
|
360
889
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -371,11 +900,21 @@ var AzureTtsClient = class {
|
|
|
371
900
|
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
372
901
|
return synthesizeSpeech(ssml, config);
|
|
373
902
|
}
|
|
374
|
-
async synthesizeSsml(ssml) {
|
|
903
|
+
async synthesizeSsml(ssml, options = {}) {
|
|
375
904
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
376
905
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
377
906
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
378
|
-
return synthesizeSsml(ssml, {
|
|
907
|
+
return synthesizeSsml(ssml, {
|
|
908
|
+
endpoint,
|
|
909
|
+
region,
|
|
910
|
+
subscriptionKey,
|
|
911
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
912
|
+
signal: options.signal ?? signal,
|
|
913
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
914
|
+
sourceNodePath: options.sourceNodePath,
|
|
915
|
+
sourceTextSegments: options.sourceTextSegments,
|
|
916
|
+
sourceMarkers: options.sourceMarkers
|
|
917
|
+
});
|
|
379
918
|
}
|
|
380
919
|
async synthesizeChunks(chunks, options = {}) {
|
|
381
920
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
@@ -384,15 +923,28 @@ var AzureTtsClient = class {
|
|
|
384
923
|
endpoint,
|
|
385
924
|
region,
|
|
386
925
|
subscriptionKey,
|
|
387
|
-
outputFormat,
|
|
388
|
-
signal,
|
|
389
|
-
timeoutMs,
|
|
926
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
927
|
+
signal: options.signal ?? signal,
|
|
928
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
929
|
+
sourceNodePath: options.sourceNodePath,
|
|
390
930
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
391
931
|
});
|
|
392
932
|
}
|
|
393
933
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
394
934
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
395
935
|
}
|
|
936
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
937
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
938
|
+
...options,
|
|
939
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
940
|
+
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
941
|
+
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
942
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
946
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
947
|
+
}
|
|
396
948
|
};
|
|
397
949
|
_options = new WeakMap();
|
|
398
950
|
|
|
@@ -448,6 +1000,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
448
1000
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
449
1001
|
const styles = stringList(record.StyleList);
|
|
450
1002
|
const status = normalizeStatus(record.Status);
|
|
1003
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
1004
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
1005
|
+
const models = stringList(record.Models);
|
|
451
1006
|
const merged = {
|
|
452
1007
|
name: existing?.name ?? name,
|
|
453
1008
|
locale: existing?.locale ?? locale,
|
|
@@ -457,6 +1012,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
457
1012
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
458
1013
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
459
1014
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
1015
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
1016
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
1017
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
1018
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
1019
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
1020
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
460
1021
|
if (status) merged.status = status;
|
|
461
1022
|
else if (existing?.status) merged.status = existing.status;
|
|
462
1023
|
voices.set(key, merged);
|
|
@@ -478,11 +1039,22 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
478
1039
|
AzureTtsClient,
|
|
479
1040
|
AzureTtsError,
|
|
480
1041
|
AzureTtsSdkError,
|
|
1042
|
+
ChunkValidationError,
|
|
1043
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
1044
|
+
MergeError,
|
|
1045
|
+
SynthesisCancelledError,
|
|
1046
|
+
SynthesisTimeoutError,
|
|
1047
|
+
UnsupportedMergeFormatError,
|
|
1048
|
+
canMergeAudioFormat,
|
|
481
1049
|
fetchAzureVoiceCatalog,
|
|
1050
|
+
mergeAudioBuffers,
|
|
482
1051
|
mergeSynthesisResults,
|
|
1052
|
+
resolveMergeAudioFormat,
|
|
1053
|
+
resolveMimeType,
|
|
483
1054
|
synthesizeSpeech,
|
|
484
1055
|
synthesizeSsml,
|
|
485
1056
|
synthesizeSsmlChunks,
|
|
1057
|
+
synthesizeSsmlChunksSafe,
|
|
486
1058
|
synthesizeSsmlSafe
|
|
487
1059
|
});
|
|
488
1060
|
//# sourceMappingURL=index.js.map
|