@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.mjs
CHANGED
|
@@ -10,6 +10,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
10
10
|
var AzureTtsError = class extends Error {
|
|
11
11
|
constructor(status, statusText, responseBody, requestId) {
|
|
12
12
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
13
|
+
this.kind = "azure-api-error";
|
|
13
14
|
this.name = "AzureTtsError";
|
|
14
15
|
this.status = status;
|
|
15
16
|
this.statusText = statusText;
|
|
@@ -25,6 +26,44 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
25
26
|
this.errorDetails = errorDetails;
|
|
26
27
|
}
|
|
27
28
|
};
|
|
29
|
+
var SynthesisCancelledError = class extends Error {
|
|
30
|
+
constructor(message = "Speech synthesis was cancelled.") {
|
|
31
|
+
super(message);
|
|
32
|
+
this.kind = "cancelled";
|
|
33
|
+
this.name = "SynthesisCancelledError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var SynthesisTimeoutError = class extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.kind = "timeout";
|
|
40
|
+
this.name = "SynthesisTimeoutError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var MergeError = class extends Error {
|
|
44
|
+
constructor(message, cause) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.kind = "merge-error";
|
|
47
|
+
this.name = "MergeError";
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var UnsupportedMergeFormatError = class extends Error {
|
|
52
|
+
constructor(format) {
|
|
53
|
+
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
54
|
+
this.kind = "unsupported-format-error";
|
|
55
|
+
this.name = "UnsupportedMergeFormatError";
|
|
56
|
+
this.format = format;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function toSynthesisError(error) {
|
|
60
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
61
|
+
return error;
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
64
|
+
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
65
|
+
return createSpeechSdkError(error);
|
|
66
|
+
}
|
|
28
67
|
function createSpeechSdkError(error) {
|
|
29
68
|
const message = error instanceof Error ? error.message : String(error);
|
|
30
69
|
return new AzureTtsSdkError(message);
|
|
@@ -32,9 +71,7 @@ function createSpeechSdkError(error) {
|
|
|
32
71
|
|
|
33
72
|
// src/synthesis.ts
|
|
34
73
|
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
35
|
-
|
|
36
|
-
// src/speechConfig.ts
|
|
37
|
-
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
74
|
+
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
38
75
|
|
|
39
76
|
// src/outputFormats.ts
|
|
40
77
|
import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
|
|
@@ -80,6 +117,14 @@ var OUTPUT_FORMATS = {
|
|
|
80
117
|
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
81
118
|
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
82
119
|
};
|
|
120
|
+
function resolveMimeType(outputFormat) {
|
|
121
|
+
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
122
|
+
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
123
|
+
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
124
|
+
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
125
|
+
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
126
|
+
return "application/octet-stream";
|
|
127
|
+
}
|
|
83
128
|
function resolveOutputFormat(outputFormat) {
|
|
84
129
|
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
85
130
|
if (resolvedFormat === void 0) {
|
|
@@ -89,6 +134,7 @@ function resolveOutputFormat(outputFormat) {
|
|
|
89
134
|
}
|
|
90
135
|
|
|
91
136
|
// src/speechConfig.ts
|
|
137
|
+
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
92
138
|
function resolveEndpoint(config) {
|
|
93
139
|
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
94
140
|
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
@@ -102,6 +148,156 @@ function createSpeechConfig(config) {
|
|
|
102
148
|
}
|
|
103
149
|
|
|
104
150
|
// src/synthesis.ts
|
|
151
|
+
function ascii(bytes, offset, value) {
|
|
152
|
+
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
153
|
+
}
|
|
154
|
+
function readUint32(bytes, offset) {
|
|
155
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
156
|
+
}
|
|
157
|
+
function parseWav(buffer) {
|
|
158
|
+
const bytes = new Uint8Array(buffer);
|
|
159
|
+
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
160
|
+
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
161
|
+
}
|
|
162
|
+
const chunks = [];
|
|
163
|
+
const dataParts = [];
|
|
164
|
+
let format;
|
|
165
|
+
let offset = 12;
|
|
166
|
+
while (offset < bytes.byteLength) {
|
|
167
|
+
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
168
|
+
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
169
|
+
const size = readUint32(bytes, offset + 4);
|
|
170
|
+
const dataStart = offset + 8;
|
|
171
|
+
const dataEnd = dataStart + size;
|
|
172
|
+
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
173
|
+
const data2 = bytes.slice(dataStart, dataEnd);
|
|
174
|
+
chunks.push({ id, data: data2 });
|
|
175
|
+
if (id === "fmt ") format ?? (format = data2);
|
|
176
|
+
if (id === "data") dataParts.push(data2);
|
|
177
|
+
offset = dataEnd + (size & 1);
|
|
178
|
+
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
179
|
+
}
|
|
180
|
+
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
181
|
+
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
182
|
+
const data = new Uint8Array(dataLength);
|
|
183
|
+
let dataOffset = 0;
|
|
184
|
+
for (const part of dataParts) {
|
|
185
|
+
data.set(part, dataOffset);
|
|
186
|
+
dataOffset += part.byteLength;
|
|
187
|
+
}
|
|
188
|
+
return { chunks, data, format };
|
|
189
|
+
}
|
|
190
|
+
function writeUint32(target, offset, value) {
|
|
191
|
+
new DataView(target.buffer).setUint32(offset, value, true);
|
|
192
|
+
}
|
|
193
|
+
function writeChunk(target, offset, id, data) {
|
|
194
|
+
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
195
|
+
writeUint32(target, offset + 4, data.byteLength);
|
|
196
|
+
target.set(data, offset + 8);
|
|
197
|
+
const end = offset + 8 + data.byteLength;
|
|
198
|
+
if (data.byteLength & 1) target[end] = 0;
|
|
199
|
+
return end + (data.byteLength & 1);
|
|
200
|
+
}
|
|
201
|
+
function mergeWavBuffers(buffers) {
|
|
202
|
+
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
203
|
+
const parsed = buffers.map(parseWav);
|
|
204
|
+
const first = parsed[0];
|
|
205
|
+
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
206
|
+
if (parsed.some(
|
|
207
|
+
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
208
|
+
))
|
|
209
|
+
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
210
|
+
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
211
|
+
const nonDataLength = first.chunks.reduce(
|
|
212
|
+
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
213
|
+
0
|
|
214
|
+
);
|
|
215
|
+
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
216
|
+
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
217
|
+
const output = new Uint8Array(outputLength);
|
|
218
|
+
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
219
|
+
writeUint32(output, 4, outputLength - 8);
|
|
220
|
+
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
221
|
+
let outputOffset = 12;
|
|
222
|
+
let dataWritten = false;
|
|
223
|
+
for (const chunk of first.chunks) {
|
|
224
|
+
if (chunk.id === "data") {
|
|
225
|
+
if (dataWritten) continue;
|
|
226
|
+
const data = new Uint8Array(dataLength);
|
|
227
|
+
let dataOffset = 0;
|
|
228
|
+
for (const item of parsed) {
|
|
229
|
+
data.set(item.data, dataOffset);
|
|
230
|
+
dataOffset += item.data.byteLength;
|
|
231
|
+
}
|
|
232
|
+
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
233
|
+
dataWritten = true;
|
|
234
|
+
} else {
|
|
235
|
+
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
239
|
+
return output.buffer;
|
|
240
|
+
}
|
|
241
|
+
function skipId3v2(bytes) {
|
|
242
|
+
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
243
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
244
|
+
const hasFooter = (bytes[5] & 16) !== 0;
|
|
245
|
+
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
246
|
+
}
|
|
247
|
+
function stripMp3Tags(buffer) {
|
|
248
|
+
const bytes = new Uint8Array(buffer);
|
|
249
|
+
const start = skipId3v2(bytes);
|
|
250
|
+
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
251
|
+
return bytes.slice(Math.min(start, end), end);
|
|
252
|
+
}
|
|
253
|
+
function isMp3Format(format) {
|
|
254
|
+
return /(?:mp3|mpeg)/i.test(format);
|
|
255
|
+
}
|
|
256
|
+
function isWavFormat(format) {
|
|
257
|
+
return /(?:wav|wave|riff)/i.test(format);
|
|
258
|
+
}
|
|
259
|
+
function isRawFormat(format) {
|
|
260
|
+
return /^raw(?:-|$)/i.test(format);
|
|
261
|
+
}
|
|
262
|
+
function resolveMergeAudioFormat(format) {
|
|
263
|
+
if (isWavFormat(format)) return "wav";
|
|
264
|
+
if (isMp3Format(format)) return "mp3";
|
|
265
|
+
if (isRawFormat(format)) return "raw";
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
function canMergeAudioFormat(format) {
|
|
269
|
+
return resolveMergeAudioFormat(format) !== void 0;
|
|
270
|
+
}
|
|
271
|
+
function mergeAudioBuffers(buffers, options) {
|
|
272
|
+
const format = typeof options === "string" ? options : options?.format;
|
|
273
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
274
|
+
try {
|
|
275
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
276
|
+
if (isMp3Format(format)) {
|
|
277
|
+
const parts = buffers.map(stripMp3Tags);
|
|
278
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
279
|
+
let offset = 0;
|
|
280
|
+
for (const part of parts) {
|
|
281
|
+
output.set(part, offset);
|
|
282
|
+
offset += part.byteLength;
|
|
283
|
+
}
|
|
284
|
+
return output.buffer;
|
|
285
|
+
}
|
|
286
|
+
if (isRawFormat(format)) {
|
|
287
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
288
|
+
let offset = 0;
|
|
289
|
+
for (const buffer of buffers) {
|
|
290
|
+
output.set(new Uint8Array(buffer), offset);
|
|
291
|
+
offset += buffer.byteLength;
|
|
292
|
+
}
|
|
293
|
+
return output.buffer;
|
|
294
|
+
}
|
|
295
|
+
throw new UnsupportedMergeFormatError(format);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
298
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
105
301
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
106
302
|
try {
|
|
107
303
|
synthesizer.close();
|
|
@@ -115,7 +311,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
115
311
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
116
312
|
async function synthesizeSsml(ssml, config) {
|
|
117
313
|
if (config.signal?.aborted) {
|
|
118
|
-
throw
|
|
314
|
+
throw new SynthesisCancelledError();
|
|
119
315
|
}
|
|
120
316
|
const speechConfig = createSpeechConfig(config);
|
|
121
317
|
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
@@ -138,23 +334,94 @@ async function synthesizeSsml(ssml, config) {
|
|
|
138
334
|
settled = true;
|
|
139
335
|
cleanup();
|
|
140
336
|
closeResources();
|
|
141
|
-
reject(
|
|
337
|
+
reject(toSynthesisError(error));
|
|
142
338
|
};
|
|
143
339
|
const boundaries = [];
|
|
144
340
|
const visemes = [];
|
|
145
341
|
const bookmarks = [];
|
|
342
|
+
let sourceEventCursor = 0;
|
|
343
|
+
let generatedSourceMap;
|
|
344
|
+
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
345
|
+
try {
|
|
346
|
+
generatedSourceMap = getSsmlSourceMap(ssml);
|
|
347
|
+
} catch {
|
|
348
|
+
generatedSourceMap = void 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
352
|
+
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
353
|
+
...segment,
|
|
354
|
+
range: {
|
|
355
|
+
start: segment.range.start + sourceBaseOffset,
|
|
356
|
+
end: segment.range.end + sourceBaseOffset
|
|
357
|
+
},
|
|
358
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
359
|
+
})) ?? [];
|
|
360
|
+
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
361
|
+
...marker,
|
|
362
|
+
originalTextRange: {
|
|
363
|
+
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
364
|
+
end: marker.originalTextRange.end + sourceBaseOffset
|
|
365
|
+
},
|
|
366
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
367
|
+
})) ?? [];
|
|
368
|
+
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
369
|
+
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
370
|
+
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
371
|
+
if (marker) {
|
|
372
|
+
return {
|
|
373
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
374
|
+
sourceNodePath: [...marker.sourceNodePath],
|
|
375
|
+
textRange: { ...marker.originalTextRange }
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) return {};
|
|
379
|
+
const value = text ?? "";
|
|
380
|
+
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
381
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value)
|
|
382
|
+
localStart = -1;
|
|
383
|
+
if (localStart < 0 || localStart > sourceText.length) {
|
|
384
|
+
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
385
|
+
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
386
|
+
}
|
|
387
|
+
localStart = Math.max(0, localStart);
|
|
388
|
+
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
389
|
+
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
390
|
+
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
391
|
+
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
392
|
+
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);
|
|
393
|
+
return {
|
|
394
|
+
originalTextRange: { ...fallbackRange },
|
|
395
|
+
textRange: { ...fallbackRange },
|
|
396
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
397
|
+
};
|
|
398
|
+
};
|
|
146
399
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
147
400
|
boundaries.push({
|
|
148
401
|
text: event.text,
|
|
149
402
|
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
150
|
-
durationMs: ticksToMilliseconds(event.duration)
|
|
403
|
+
durationMs: ticksToMilliseconds(event.duration),
|
|
404
|
+
...mapSourceEvent(
|
|
405
|
+
event.text,
|
|
406
|
+
event.textOffset
|
|
407
|
+
)
|
|
151
408
|
});
|
|
152
409
|
};
|
|
153
410
|
synthesizer.visemeReceived = (_sender, event) => {
|
|
154
|
-
|
|
411
|
+
const eventWithOffset = event;
|
|
412
|
+
visemes.push({
|
|
413
|
+
visemeId: event.visemeId,
|
|
414
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
415
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
416
|
+
});
|
|
155
417
|
};
|
|
156
418
|
synthesizer.bookmarkReached = (_sender, event) => {
|
|
157
|
-
|
|
419
|
+
const eventWithOffset = event;
|
|
420
|
+
bookmarks.push({
|
|
421
|
+
name: event.text,
|
|
422
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
423
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
424
|
+
});
|
|
158
425
|
};
|
|
159
426
|
const cb = (result) => {
|
|
160
427
|
if (settled) return;
|
|
@@ -177,7 +444,10 @@ async function synthesizeSsml(ssml, config) {
|
|
|
177
444
|
const requestId = result.resultId;
|
|
178
445
|
const addSourceMetadata = (event) => ({
|
|
179
446
|
...event,
|
|
180
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
447
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
448
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
449
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
450
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
181
451
|
...requestId ? { requestId } : {}
|
|
182
452
|
});
|
|
183
453
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
@@ -195,12 +465,12 @@ async function synthesizeSsml(ssml, config) {
|
|
|
195
465
|
};
|
|
196
466
|
try {
|
|
197
467
|
if (config.signal) {
|
|
198
|
-
abortHandler = () => rejectWithError(
|
|
468
|
+
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
199
469
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
200
470
|
}
|
|
201
471
|
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
202
472
|
timeout = setTimeout(
|
|
203
|
-
() => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
|
|
473
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
|
|
204
474
|
config.timeoutMs
|
|
205
475
|
);
|
|
206
476
|
}
|
|
@@ -213,60 +483,117 @@ async function synthesizeSsml(ssml, config) {
|
|
|
213
483
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
214
484
|
const results = [];
|
|
215
485
|
const totalChunks = chunks.length;
|
|
486
|
+
const report = (event) => config.onProgress?.(event);
|
|
216
487
|
for (const [index, chunk] of chunks.entries()) {
|
|
217
488
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
489
|
+
report({
|
|
490
|
+
currentChunk: index,
|
|
491
|
+
totalChunks,
|
|
492
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
493
|
+
chunkIndex: index,
|
|
494
|
+
originalTextRange: input.originalTextRange,
|
|
495
|
+
status: "pending",
|
|
496
|
+
durationMs: 0
|
|
222
497
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
498
|
+
}
|
|
499
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
500
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
501
|
+
report({
|
|
502
|
+
currentChunk: index,
|
|
226
503
|
totalChunks,
|
|
227
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
504
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
505
|
+
chunkIndex: index,
|
|
506
|
+
originalTextRange: input.originalTextRange,
|
|
507
|
+
status: "synthesizing",
|
|
508
|
+
durationMs: 0
|
|
228
509
|
});
|
|
510
|
+
const startedAt = Date.now();
|
|
511
|
+
try {
|
|
512
|
+
const result = await synthesizeSsml(input.ssml, {
|
|
513
|
+
...config,
|
|
514
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
515
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
516
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
517
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
518
|
+
chunkIndex: index,
|
|
519
|
+
onProgress: void 0
|
|
520
|
+
});
|
|
521
|
+
results.push(result);
|
|
522
|
+
report({
|
|
523
|
+
currentChunk: index + 1,
|
|
524
|
+
totalChunks,
|
|
525
|
+
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
526
|
+
chunkIndex: index,
|
|
527
|
+
originalTextRange: input.originalTextRange,
|
|
528
|
+
status: "success",
|
|
529
|
+
durationMs: Date.now() - startedAt
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
report({
|
|
533
|
+
currentChunk: index,
|
|
534
|
+
totalChunks,
|
|
535
|
+
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
536
|
+
chunkIndex: index,
|
|
537
|
+
originalTextRange: input.originalTextRange,
|
|
538
|
+
status: "failed",
|
|
539
|
+
durationMs: Date.now() - startedAt,
|
|
540
|
+
error
|
|
541
|
+
});
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
229
544
|
}
|
|
230
|
-
return mergeSynthesisResults(results
|
|
545
|
+
return mergeSynthesisResults(results, {
|
|
546
|
+
format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
547
|
+
});
|
|
231
548
|
}
|
|
232
|
-
function
|
|
233
|
-
const audioLength = results.reduce((total, result) => total + result.audioData.byteLength, 0);
|
|
234
|
-
const audioData = new Uint8Array(audioLength);
|
|
549
|
+
function createMergedResult(results, audioData, format) {
|
|
235
550
|
const boundaries = [];
|
|
236
551
|
const visemes = [];
|
|
237
552
|
const bookmarks = [];
|
|
238
|
-
let byteOffset = 0;
|
|
239
553
|
let durationOffset = 0;
|
|
240
|
-
for (const result of results) {
|
|
241
|
-
audioData.set(new Uint8Array(result.audioData), byteOffset);
|
|
242
|
-
byteOffset += result.audioData.byteLength;
|
|
554
|
+
for (const [resultIndex, result] of results.entries()) {
|
|
243
555
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
244
556
|
for (const boundary of chunkBoundaries) {
|
|
245
557
|
const textRange = boundary.textRange ?? result.textRange;
|
|
558
|
+
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
246
559
|
const requestId = boundary.requestId ?? result.requestId;
|
|
247
560
|
boundaries.push({
|
|
248
561
|
...boundary,
|
|
249
562
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
563
|
+
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
564
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
565
|
+
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
566
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
250
567
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
251
568
|
...requestId ? { requestId } : {}
|
|
252
569
|
});
|
|
253
570
|
}
|
|
254
571
|
for (const viseme of result.visemes ?? []) {
|
|
255
572
|
const textRange = viseme.textRange ?? result.textRange;
|
|
573
|
+
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
256
574
|
const requestId = viseme.requestId ?? result.requestId;
|
|
257
575
|
visemes.push({
|
|
258
576
|
...viseme,
|
|
259
577
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
578
|
+
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
579
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
580
|
+
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
581
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
260
582
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
261
583
|
...requestId ? { requestId } : {}
|
|
262
584
|
});
|
|
263
585
|
}
|
|
264
586
|
for (const bookmark of result.bookmarks ?? []) {
|
|
265
587
|
const textRange = bookmark.textRange ?? result.textRange;
|
|
588
|
+
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
266
589
|
const requestId = bookmark.requestId ?? result.requestId;
|
|
267
590
|
bookmarks.push({
|
|
268
591
|
...bookmark,
|
|
269
592
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
593
|
+
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
594
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
595
|
+
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
596
|
+
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
270
597
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
271
598
|
...requestId ? { requestId } : {}
|
|
272
599
|
});
|
|
@@ -274,8 +601,9 @@ function mergeSynthesisResults(results) {
|
|
|
274
601
|
durationOffset += Math.max(0, result.durationMs);
|
|
275
602
|
}
|
|
276
603
|
return {
|
|
277
|
-
audioData
|
|
604
|
+
audioData,
|
|
278
605
|
durationMs: durationOffset,
|
|
606
|
+
mimeType: resolveMimeType(format),
|
|
279
607
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
280
608
|
...visemes.length > 0 ? { visemes } : {},
|
|
281
609
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -283,35 +611,225 @@ function mergeSynthesisResults(results) {
|
|
|
283
611
|
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
284
612
|
};
|
|
285
613
|
}
|
|
614
|
+
function mergeSynthesisResults(results, options) {
|
|
615
|
+
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
616
|
+
const format = resolvedOptions?.format;
|
|
617
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
618
|
+
const buffers = results.map((result) => result.audioData);
|
|
619
|
+
if (resolvedOptions.customMerger) {
|
|
620
|
+
return Promise.resolve().then(() => resolvedOptions.customMerger?.(buffers, format)).then((merged) => {
|
|
621
|
+
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
622
|
+
return createMergedResult(results, merged, format);
|
|
623
|
+
}).catch((error) => {
|
|
624
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
625
|
+
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
return createMergedResult(results, mergeAudioBuffers(buffers, { format }), format);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError) throw error;
|
|
632
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
286
635
|
async function synthesizeSpeech(ssml, config) {
|
|
287
636
|
return (await synthesizeSsml(ssml, config)).audioData;
|
|
288
637
|
}
|
|
289
638
|
|
|
290
639
|
// src/safe.ts
|
|
291
640
|
import { validateAzureSsml } from "@ssml-builder-js/ssml-core";
|
|
641
|
+
var ChunkValidationError = class extends Error {
|
|
642
|
+
constructor(chunkIndex, diagnostics) {
|
|
643
|
+
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
644
|
+
this.kind = "validation-error";
|
|
645
|
+
this.name = "ChunkValidationError";
|
|
646
|
+
this.chunkIndex = chunkIndex;
|
|
647
|
+
this.diagnostics = diagnostics;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
function failure(error) {
|
|
651
|
+
return { ok: false, success: false, status: error.kind, error };
|
|
652
|
+
}
|
|
292
653
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
293
|
-
const validationOptions = options.validation ?? options;
|
|
654
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
294
655
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
656
|
+
if (options.signal?.aborted) {
|
|
657
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
658
|
+
return failure(error);
|
|
659
|
+
}
|
|
295
660
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
296
661
|
if (errors.length > 0) {
|
|
662
|
+
return failure({
|
|
663
|
+
kind: "validation-error",
|
|
664
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
665
|
+
diagnostics: errors
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
297
669
|
return {
|
|
298
|
-
ok:
|
|
299
|
-
success:
|
|
300
|
-
status: "
|
|
301
|
-
|
|
302
|
-
kind: "validation",
|
|
303
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
304
|
-
diagnostics: errors
|
|
305
|
-
}
|
|
670
|
+
ok: true,
|
|
671
|
+
success: true,
|
|
672
|
+
status: "success",
|
|
673
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal })
|
|
306
674
|
};
|
|
675
|
+
} catch (error) {
|
|
676
|
+
const synthesisError = toSynthesisError(error);
|
|
677
|
+
return failure(synthesisError);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
681
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
682
|
+
if (options.signal?.aborted) {
|
|
683
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
684
|
+
return failure(error);
|
|
685
|
+
}
|
|
686
|
+
const pending = (index, status, error) => {
|
|
687
|
+
options.onProgress?.({
|
|
688
|
+
currentChunk: status === "success" ? index + 1 : index,
|
|
689
|
+
totalChunks: chunks.length,
|
|
690
|
+
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
691
|
+
chunkIndex: index,
|
|
692
|
+
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
693
|
+
status,
|
|
694
|
+
durationMs: 0,
|
|
695
|
+
...error ? { error } : {}
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
chunks.forEach((_chunk, index) => {
|
|
699
|
+
pending(index, "pending");
|
|
700
|
+
});
|
|
701
|
+
const validations = await Promise.all(
|
|
702
|
+
chunks.map(async (chunk) => {
|
|
703
|
+
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
704
|
+
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
705
|
+
const diagnostics = await Promise.resolve(
|
|
706
|
+
validateAzureSsml(ssml, { ...validationOptions, ...sourceNodePath ? { sourceNodePath } : {} })
|
|
707
|
+
);
|
|
708
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
712
|
+
if (firstInvalidIndex >= 0) {
|
|
713
|
+
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
714
|
+
pending(firstInvalidIndex, "failed", error);
|
|
715
|
+
return failure(error);
|
|
307
716
|
}
|
|
308
717
|
try {
|
|
309
|
-
|
|
718
|
+
if (client.synthesizeChunks) {
|
|
719
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
720
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
721
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
722
|
+
});
|
|
723
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
724
|
+
onProgress: options.onProgress,
|
|
725
|
+
outputFormat: options.outputFormat,
|
|
726
|
+
signal: options.signal,
|
|
727
|
+
timeoutMs: options.timeoutMs,
|
|
728
|
+
sourceNodePath: options.sourceNodePath
|
|
729
|
+
});
|
|
730
|
+
return { ok: true, success: true, status: "success", value };
|
|
731
|
+
}
|
|
732
|
+
const results = [];
|
|
733
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
734
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
735
|
+
const sourceNodePath = input.sourceNodePath;
|
|
736
|
+
const originalTextRange = input.originalTextRange;
|
|
737
|
+
pending(index, "synthesizing");
|
|
738
|
+
const startedAt = Date.now();
|
|
739
|
+
try {
|
|
740
|
+
const result = await client.synthesizeSsml(input.ssml, {
|
|
741
|
+
outputFormat: options.outputFormat,
|
|
742
|
+
signal: options.signal,
|
|
743
|
+
timeoutMs: options.timeoutMs,
|
|
744
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
745
|
+
});
|
|
746
|
+
results.push({
|
|
747
|
+
...result,
|
|
748
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
749
|
+
...sourceNodePath ? {
|
|
750
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
751
|
+
...event,
|
|
752
|
+
sourceNodePath: [...sourceNodePath],
|
|
753
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
754
|
+
})),
|
|
755
|
+
visemes: result.visemes?.map((event) => ({
|
|
756
|
+
...event,
|
|
757
|
+
sourceNodePath: [...sourceNodePath],
|
|
758
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
759
|
+
})),
|
|
760
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
761
|
+
...event,
|
|
762
|
+
sourceNodePath: [...sourceNodePath],
|
|
763
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
764
|
+
}))
|
|
765
|
+
} : {},
|
|
766
|
+
...originalTextRange ? {
|
|
767
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
768
|
+
...event,
|
|
769
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
770
|
+
})),
|
|
771
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
772
|
+
...event,
|
|
773
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
774
|
+
})),
|
|
775
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
776
|
+
...event,
|
|
777
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
778
|
+
})),
|
|
779
|
+
visemes: result.visemes?.map((event) => ({
|
|
780
|
+
...event,
|
|
781
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
782
|
+
})),
|
|
783
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
784
|
+
...event,
|
|
785
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
786
|
+
}))
|
|
787
|
+
} : {}
|
|
788
|
+
});
|
|
789
|
+
options.onProgress?.({
|
|
790
|
+
currentChunk: index + 1,
|
|
791
|
+
totalChunks: chunks.length,
|
|
792
|
+
percent: chunks.length === 0 ? 100 : Math.round((index + 1) / chunks.length * 100),
|
|
793
|
+
chunkIndex: index,
|
|
794
|
+
originalTextRange: input.originalTextRange,
|
|
795
|
+
status: "success",
|
|
796
|
+
durationMs: Date.now() - startedAt
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
options.onProgress?.({
|
|
800
|
+
currentChunk: index,
|
|
801
|
+
totalChunks: chunks.length,
|
|
802
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
803
|
+
chunkIndex: index,
|
|
804
|
+
originalTextRange: input.originalTextRange,
|
|
805
|
+
status: "failed",
|
|
806
|
+
durationMs: Date.now() - startedAt,
|
|
807
|
+
error
|
|
808
|
+
});
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
ok: true,
|
|
814
|
+
success: true,
|
|
815
|
+
status: "success",
|
|
816
|
+
value: mergeSynthesisResults(results, {
|
|
817
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
818
|
+
})
|
|
819
|
+
};
|
|
310
820
|
} catch (error) {
|
|
311
|
-
const
|
|
312
|
-
return
|
|
821
|
+
const synthesisError = toSynthesisError(error);
|
|
822
|
+
return failure(synthesisError);
|
|
313
823
|
}
|
|
314
824
|
}
|
|
825
|
+
function withValidationSignal(options, signal) {
|
|
826
|
+
if (!signal) return options;
|
|
827
|
+
return {
|
|
828
|
+
...options,
|
|
829
|
+
urlValidatorSignal: signal,
|
|
830
|
+
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
831
|
+
};
|
|
832
|
+
}
|
|
315
833
|
|
|
316
834
|
// src/client.ts
|
|
317
835
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -328,11 +846,21 @@ var AzureTtsClient = class {
|
|
|
328
846
|
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
329
847
|
return synthesizeSpeech(ssml, config);
|
|
330
848
|
}
|
|
331
|
-
async synthesizeSsml(ssml) {
|
|
849
|
+
async synthesizeSsml(ssml, options = {}) {
|
|
332
850
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
333
851
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
334
852
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
335
|
-
return synthesizeSsml(ssml, {
|
|
853
|
+
return synthesizeSsml(ssml, {
|
|
854
|
+
endpoint,
|
|
855
|
+
region,
|
|
856
|
+
subscriptionKey,
|
|
857
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
858
|
+
signal: options.signal ?? signal,
|
|
859
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
860
|
+
sourceNodePath: options.sourceNodePath,
|
|
861
|
+
sourceTextSegments: options.sourceTextSegments,
|
|
862
|
+
sourceMarkers: options.sourceMarkers
|
|
863
|
+
});
|
|
336
864
|
}
|
|
337
865
|
async synthesizeChunks(chunks, options = {}) {
|
|
338
866
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
@@ -341,15 +869,28 @@ var AzureTtsClient = class {
|
|
|
341
869
|
endpoint,
|
|
342
870
|
region,
|
|
343
871
|
subscriptionKey,
|
|
344
|
-
outputFormat,
|
|
345
|
-
signal,
|
|
346
|
-
timeoutMs,
|
|
872
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
873
|
+
signal: options.signal ?? signal,
|
|
874
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
875
|
+
sourceNodePath: options.sourceNodePath,
|
|
347
876
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
348
877
|
});
|
|
349
878
|
}
|
|
350
879
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
351
880
|
return synthesizeSsmlSafe(this, ssml, options);
|
|
352
881
|
}
|
|
882
|
+
async synthesizeChunksSafe(chunks, options = {}) {
|
|
883
|
+
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
884
|
+
...options,
|
|
885
|
+
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
886
|
+
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
887
|
+
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
888
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
892
|
+
return this.synthesizeChunksSafe(chunks, options);
|
|
893
|
+
}
|
|
353
894
|
};
|
|
354
895
|
_options = new WeakMap();
|
|
355
896
|
|
|
@@ -405,6 +946,9 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
405
946
|
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
406
947
|
const styles = stringList(record.StyleList);
|
|
407
948
|
const status = normalizeStatus(record.Status);
|
|
949
|
+
const supportedTags = stringList(record.SupportedTags);
|
|
950
|
+
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
951
|
+
const models = stringList(record.Models);
|
|
408
952
|
const merged = {
|
|
409
953
|
name: existing?.name ?? name,
|
|
410
954
|
locale: existing?.locale ?? locale,
|
|
@@ -414,6 +958,12 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
414
958
|
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
415
959
|
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
416
960
|
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
961
|
+
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
962
|
+
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
963
|
+
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
964
|
+
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
965
|
+
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
966
|
+
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
417
967
|
if (status) merged.status = status;
|
|
418
968
|
else if (existing?.status) merged.status = existing.status;
|
|
419
969
|
voices.set(key, merged);
|
|
@@ -434,11 +984,22 @@ export {
|
|
|
434
984
|
AzureTtsClient,
|
|
435
985
|
AzureTtsError,
|
|
436
986
|
AzureTtsSdkError,
|
|
987
|
+
ChunkValidationError,
|
|
988
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
989
|
+
MergeError,
|
|
990
|
+
SynthesisCancelledError,
|
|
991
|
+
SynthesisTimeoutError,
|
|
992
|
+
UnsupportedMergeFormatError,
|
|
993
|
+
canMergeAudioFormat,
|
|
437
994
|
fetchAzureVoiceCatalog,
|
|
995
|
+
mergeAudioBuffers,
|
|
438
996
|
mergeSynthesisResults,
|
|
997
|
+
resolveMergeAudioFormat,
|
|
998
|
+
resolveMimeType,
|
|
439
999
|
synthesizeSpeech,
|
|
440
1000
|
synthesizeSsml,
|
|
441
1001
|
synthesizeSsmlChunks,
|
|
1002
|
+
synthesizeSsmlChunksSafe,
|
|
442
1003
|
synthesizeSsmlSafe
|
|
443
1004
|
};
|
|
444
1005
|
//# sourceMappingURL=index.mjs.map
|