ssml-builder-js 2.17.0 → 2.18.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 +4 -0
- package/dist/{index.d-tpKoP1jl.d.mts → index.d-B2WddTa4.d.mts} +1 -1
- package/dist/{index.d-tpKoP1jl.d.ts → index.d-B2WddTa4.d.ts} +1 -1
- package/dist/index.d.mts +169 -140
- package/dist/index.d.ts +169 -140
- package/dist/index.js +250 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -56
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -180,6 +180,9 @@ var OUTPUT_FORMATS = {
|
|
|
180
180
|
function resolveMimeType(outputFormat) {
|
|
181
181
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
182
182
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
183
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
184
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
185
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
183
186
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
184
187
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
185
188
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -208,6 +211,27 @@ function createSpeechConfig(config) {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
// packages/azure-tts-client/src/synthesis.ts
|
|
214
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT) {
|
|
215
|
+
const readAttribute = (name) => {
|
|
216
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
217
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
218
|
+
};
|
|
219
|
+
const payload = JSON.stringify({
|
|
220
|
+
ssml,
|
|
221
|
+
outputFormat,
|
|
222
|
+
voice: readAttribute("(?:name|voice)"),
|
|
223
|
+
language: readAttribute("(?:xml:lang|lang)"),
|
|
224
|
+
rate: readAttribute("rate"),
|
|
225
|
+
pitch: readAttribute("pitch")
|
|
226
|
+
});
|
|
227
|
+
let hash = 0xcbf29ce484222325n;
|
|
228
|
+
const mask = 0xffffffffffffffffn;
|
|
229
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
230
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
231
|
+
hash = hash * 0x100000001b3n & mask;
|
|
232
|
+
}
|
|
233
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
234
|
+
}
|
|
211
235
|
function ascii(bytes, offset, value) {
|
|
212
236
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
213
237
|
}
|
|
@@ -247,9 +271,11 @@ function parseWav(buffer) {
|
|
|
247
271
|
}
|
|
248
272
|
return { chunks, data, format };
|
|
249
273
|
}
|
|
250
|
-
function
|
|
251
|
-
const match =
|
|
252
|
-
|
|
274
|
+
function formatSampleRate(format) {
|
|
275
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
276
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
277
|
+
const value = Number(match[1]);
|
|
278
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
253
279
|
}
|
|
254
280
|
function formatChannels(format, fallback) {
|
|
255
281
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -257,11 +283,11 @@ function formatChannels(format, fallback) {
|
|
|
257
283
|
return fallback;
|
|
258
284
|
}
|
|
259
285
|
function formatAudioSpecification(format) {
|
|
260
|
-
const sampleRate =
|
|
286
|
+
const sampleRate = formatSampleRate(format);
|
|
261
287
|
const channels = formatChannels(format, 0);
|
|
262
288
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
263
289
|
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
264
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /
|
|
290
|
+
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /mulaw|mu-law/i.test(format) ? "mulaw" : /alaw|a-law/i.test(format) ? "alaw" : /siren/i.test(format) ? "siren" : /pcm/i.test(format) ? "pcm" : "unknown";
|
|
265
291
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
266
292
|
const container = /(?:wav|wave|riff)/i.test(format) ? "riff-wave" : /mp3|mpeg/i.test(format) ? "mp3-raw" : /ogg/i.test(format) ? "ogg" : /webm/i.test(format) ? "webm" : /raw/i.test(format) ? "raw" : void 0;
|
|
267
293
|
return {
|
|
@@ -274,7 +300,7 @@ function formatAudioSpecification(format) {
|
|
|
274
300
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
275
301
|
...container ? { container } : {},
|
|
276
302
|
isVbr: /vbr/i.test(format),
|
|
277
|
-
isCompressed: codec
|
|
303
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
278
304
|
};
|
|
279
305
|
}
|
|
280
306
|
function parseMp3Specification(buffer, format) {
|
|
@@ -325,27 +351,43 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
325
351
|
const channels = view.getUint16(2, true);
|
|
326
352
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
327
353
|
const formatCode = view.getUint16(0, true);
|
|
354
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
355
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
328
356
|
return {
|
|
329
357
|
format,
|
|
330
358
|
mimeType: "audio/wav",
|
|
331
|
-
codec
|
|
359
|
+
codec,
|
|
332
360
|
sampleRate,
|
|
333
361
|
channels,
|
|
334
362
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
335
363
|
bitDepth: bitsPerSample,
|
|
336
364
|
container: "riff-wave",
|
|
337
365
|
isVbr: false,
|
|
338
|
-
isCompressed:
|
|
366
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
339
367
|
};
|
|
340
368
|
}
|
|
341
369
|
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
342
|
-
|
|
370
|
+
const specification = formatAudioSpecification(format);
|
|
371
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
372
|
+
return specification;
|
|
373
|
+
}
|
|
374
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
375
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
376
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
377
|
+
}
|
|
378
|
+
if (specification.codec === "siren" || specification.codec === "silk") return;
|
|
379
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
380
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
343
385
|
}
|
|
344
386
|
function validateAudioSpecifications(specs) {
|
|
345
387
|
const first = specs[0];
|
|
346
388
|
if (!first) return;
|
|
347
389
|
const mismatch = specs.find(
|
|
348
|
-
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate || first.bitDepth !== void 0 && spec.bitDepth !== void 0 && spec.bitDepth !== first.bitDepth || first.container !== void 0 && spec.container !== void 0 && spec.container !== first.container || first.isVbr !== void 0 && spec.isVbr !== void 0 && spec.isVbr !== first.isVbr
|
|
390
|
+
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || spec.codec !== first.codec || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate || first.bitDepth !== void 0 && spec.bitDepth !== void 0 && spec.bitDepth !== first.bitDepth || first.container !== void 0 && spec.container !== void 0 && spec.container !== first.container || first.isVbr !== void 0 && spec.isVbr !== void 0 && spec.isVbr !== first.isVbr
|
|
349
391
|
);
|
|
350
392
|
if (mismatch)
|
|
351
393
|
throw new AudioFormatMismatchError(
|
|
@@ -428,6 +470,30 @@ function isWavFormat(format) {
|
|
|
428
470
|
function isRawFormat(format) {
|
|
429
471
|
return /^raw(?:-|$)/i.test(format);
|
|
430
472
|
}
|
|
473
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType) {
|
|
474
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
475
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
476
|
+
}
|
|
477
|
+
const specification = inspectAudioSpecification(merged, format);
|
|
478
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
479
|
+
const firstInput = inputSpecs[0];
|
|
480
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
481
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
482
|
+
...inputSpecs,
|
|
483
|
+
specification
|
|
484
|
+
]);
|
|
485
|
+
}
|
|
486
|
+
if (isRawFormat(format)) {
|
|
487
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
488
|
+
if (merged.byteLength !== expectedSize) {
|
|
489
|
+
throw new MergeError(
|
|
490
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
validateRawAudioBuffer(merged, specification);
|
|
494
|
+
}
|
|
495
|
+
return specification;
|
|
496
|
+
}
|
|
431
497
|
function resolveMergeAudioFormat(format) {
|
|
432
498
|
if (isWavFormat(format)) return "wav";
|
|
433
499
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -480,7 +546,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
480
546
|
}
|
|
481
547
|
}
|
|
482
548
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
483
|
-
async function
|
|
549
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
484
550
|
if (config.signal?.aborted) {
|
|
485
551
|
throw new SynthesisCancelledError();
|
|
486
552
|
}
|
|
@@ -610,6 +676,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
610
676
|
rejectWithError(err);
|
|
611
677
|
return;
|
|
612
678
|
}
|
|
679
|
+
let audioSpec;
|
|
680
|
+
try {
|
|
681
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
rejectWithError(error);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
613
686
|
settled = true;
|
|
614
687
|
cleanup();
|
|
615
688
|
closeResources();
|
|
@@ -645,8 +718,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
645
718
|
resolve({
|
|
646
719
|
audioData: result.audioData,
|
|
647
720
|
durationMs,
|
|
648
|
-
audioSpec
|
|
649
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
721
|
+
audioSpec,
|
|
722
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
650
723
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
651
724
|
...requestId ? { requestId } : {},
|
|
652
725
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -659,7 +732,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
659
732
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
660
733
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
661
734
|
}
|
|
662
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
735
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
663
736
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
664
737
|
timeout = setTimeout(
|
|
665
738
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -712,7 +785,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
712
785
|
}
|
|
713
786
|
});
|
|
714
787
|
}
|
|
715
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
788
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
716
789
|
const options = retryOptions ? {
|
|
717
790
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
718
791
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -723,17 +796,29 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
723
796
|
while (true) {
|
|
724
797
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
725
798
|
try {
|
|
726
|
-
return await
|
|
799
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
727
800
|
} catch (error) {
|
|
728
801
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
729
802
|
throw error;
|
|
730
803
|
attempt += 1;
|
|
731
804
|
const delayMs = retryDelay(options, attempt, error);
|
|
805
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
806
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
807
|
+
throw new SynthesisTimeoutError(
|
|
808
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
732
811
|
onRetry(attempt, delayMs);
|
|
733
|
-
await waitForRetry(delayMs, config.signal);
|
|
812
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
734
813
|
}
|
|
735
814
|
}
|
|
736
815
|
}
|
|
816
|
+
async function synthesizeSsml(ssml, config) {
|
|
817
|
+
const totalJobMs = config.timeouts?.totalJobMs;
|
|
818
|
+
const deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
819
|
+
if (!config.retryOptions) return synthesizeSsmlOnce(ssml, config);
|
|
820
|
+
return synthesizeWithRetry(ssml, config, config.retryOptions, () => void 0, deadlineAtMs);
|
|
821
|
+
}
|
|
737
822
|
function createAbortScope(parent, timeoutMs) {
|
|
738
823
|
const controller = new AbortController();
|
|
739
824
|
let didTimeout = false;
|
|
@@ -754,10 +839,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
754
839
|
abort: () => controller.abort()
|
|
755
840
|
};
|
|
756
841
|
}
|
|
757
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
842
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
758
843
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
759
844
|
try {
|
|
760
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
845
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
761
846
|
} catch (error) {
|
|
762
847
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
763
848
|
throw error;
|
|
@@ -766,18 +851,36 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
766
851
|
}
|
|
767
852
|
}
|
|
768
853
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
769
|
-
const results = new Array(chunks.length);
|
|
770
854
|
const totalChunks = chunks.length;
|
|
855
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
856
|
+
const fingerprints = inputs.map(
|
|
857
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT)
|
|
858
|
+
);
|
|
859
|
+
const results = new Array(totalChunks);
|
|
771
860
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
861
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
862
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
863
|
+
chunkIndex,
|
|
864
|
+
status: "pending",
|
|
865
|
+
canResume: true
|
|
866
|
+
}));
|
|
772
867
|
for (const [index, cached] of cachedChunks) {
|
|
773
|
-
if (index
|
|
868
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
869
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
870
|
+
if (isValid) {
|
|
871
|
+
results[index] = { ...cached };
|
|
872
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
873
|
+
} else {
|
|
874
|
+
invalidCachedIndices.add(index);
|
|
875
|
+
}
|
|
774
876
|
}
|
|
775
877
|
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
776
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
878
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
879
|
+
const jobStartedAt = Date.now();
|
|
880
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
777
881
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
778
882
|
const report = (event) => config.onProgress?.(event);
|
|
779
|
-
for (const [index,
|
|
780
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
883
|
+
for (const [index, input] of inputs.entries()) {
|
|
781
884
|
report({
|
|
782
885
|
currentChunk: index,
|
|
783
886
|
totalChunks,
|
|
@@ -799,8 +902,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
799
902
|
if (index >= chunks.length) return;
|
|
800
903
|
if (!shouldSynthesize(index)) continue;
|
|
801
904
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
802
|
-
const
|
|
803
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
905
|
+
const input = inputs[index];
|
|
804
906
|
report({
|
|
805
907
|
currentChunk: completed,
|
|
806
908
|
totalChunks,
|
|
@@ -837,9 +939,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
837
939
|
retryAttempt,
|
|
838
940
|
nextRetryDelayMs,
|
|
839
941
|
isRetrying: true
|
|
840
|
-
})
|
|
942
|
+
}),
|
|
943
|
+
jobDeadlineAt
|
|
841
944
|
);
|
|
842
945
|
results[index] = result;
|
|
946
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
843
947
|
completed += 1;
|
|
844
948
|
report({
|
|
845
949
|
currentChunk: completed,
|
|
@@ -851,7 +955,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
851
955
|
durationMs: Date.now() - startedAt
|
|
852
956
|
});
|
|
853
957
|
} catch (error) {
|
|
854
|
-
|
|
958
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
959
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
960
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
961
|
+
chunkStates[index] = {
|
|
962
|
+
chunkIndex: index,
|
|
963
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
964
|
+
isOriginalFailure: !wasCancelled,
|
|
965
|
+
canResume: true,
|
|
966
|
+
error
|
|
967
|
+
};
|
|
855
968
|
report({
|
|
856
969
|
currentChunk: completed,
|
|
857
970
|
totalChunks,
|
|
@@ -862,7 +975,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
862
975
|
durationMs: Date.now() - startedAt,
|
|
863
976
|
error
|
|
864
977
|
});
|
|
865
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
866
978
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
867
979
|
return;
|
|
868
980
|
}
|
|
@@ -880,11 +992,25 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
880
992
|
postMergeValidator: config.postMergeValidator
|
|
881
993
|
});
|
|
882
994
|
} catch (error) {
|
|
995
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
996
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
997
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
998
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const synthesizedChunks = results.flatMap(
|
|
1003
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1004
|
+
);
|
|
883
1005
|
const partial = {
|
|
884
|
-
synthesizedChunks
|
|
885
|
-
completedChunks:
|
|
886
|
-
pendingChunkIndices:
|
|
1006
|
+
synthesizedChunks,
|
|
1007
|
+
completedChunks: synthesizedChunks,
|
|
1008
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1009
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1010
|
+
),
|
|
887
1011
|
failedChunkIndices: [...failedIndices],
|
|
1012
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1013
|
+
chunkStates,
|
|
888
1014
|
totalChunks
|
|
889
1015
|
};
|
|
890
1016
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
@@ -982,16 +1108,15 @@ function mergeSynthesisResults(results, options) {
|
|
|
982
1108
|
})
|
|
983
1109
|
).then((merged) => {
|
|
984
1110
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
985
|
-
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
986
|
-
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
987
1111
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
988
|
-
const
|
|
989
|
-
results,
|
|
1112
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
990
1113
|
merged,
|
|
991
1114
|
format,
|
|
992
|
-
|
|
993
|
-
|
|
1115
|
+
buffers,
|
|
1116
|
+
inputSpecs,
|
|
1117
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format)
|
|
994
1118
|
);
|
|
1119
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
995
1120
|
return Promise.resolve(
|
|
996
1121
|
resolvedOptions.postMergeValidator?.(result, {
|
|
997
1122
|
format,
|
|
@@ -1120,7 +1245,7 @@ function resolveConcurrency2(value, total) {
|
|
|
1120
1245
|
if (value === Infinity) return Math.max(1, total);
|
|
1121
1246
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1122
1247
|
}
|
|
1123
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1248
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1124
1249
|
const retry = options ? {
|
|
1125
1250
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1126
1251
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -1137,6 +1262,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
1137
1262
|
throw error;
|
|
1138
1263
|
attempt += 1;
|
|
1139
1264
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1265
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1266
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1267
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1268
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1269
|
+
}
|
|
1140
1270
|
onRetry(attempt, delayMs);
|
|
1141
1271
|
if (delayMs > 0)
|
|
1142
1272
|
await new Promise((resolve, reject) => {
|
|
@@ -1184,20 +1314,24 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
1184
1314
|
diagnostics: errors
|
|
1185
1315
|
});
|
|
1186
1316
|
}
|
|
1317
|
+
const jobScope = options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts.totalJobMs) : void 0;
|
|
1187
1318
|
try {
|
|
1188
1319
|
return {
|
|
1189
1320
|
ok: true,
|
|
1190
1321
|
success: true,
|
|
1191
1322
|
status: "success",
|
|
1192
1323
|
value: await client.synthesizeSsml(ssml, {
|
|
1193
|
-
signal: options.signal,
|
|
1324
|
+
signal: jobScope?.signal ?? options.signal,
|
|
1194
1325
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
1195
1326
|
timeouts: options.timeouts
|
|
1196
1327
|
})
|
|
1197
1328
|
};
|
|
1198
1329
|
} catch (error) {
|
|
1330
|
+
if (jobScope?.timedOut()) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1199
1331
|
const synthesisError = toSynthesisError(error);
|
|
1200
1332
|
return failure(synthesisError);
|
|
1333
|
+
} finally {
|
|
1334
|
+
jobScope?.dispose();
|
|
1201
1335
|
}
|
|
1202
1336
|
}
|
|
1203
1337
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
@@ -1269,17 +1403,32 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1269
1403
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1270
1404
|
customMerger: options.customMerger,
|
|
1271
1405
|
outputMimeType: options.outputMimeType,
|
|
1272
|
-
postMergeValidator: options.postMergeValidator
|
|
1406
|
+
postMergeValidator: options.postMergeValidator,
|
|
1407
|
+
resumeValidation: options.resumeValidation
|
|
1273
1408
|
});
|
|
1274
1409
|
return { ok: true, success: true, status: "success", value };
|
|
1275
1410
|
}
|
|
1411
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1412
|
+
const fingerprints = inputs.map((chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat));
|
|
1276
1413
|
const results = new Array(chunks.length);
|
|
1414
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1415
|
+
chunkIndex,
|
|
1416
|
+
status: "pending",
|
|
1417
|
+
canResume: true
|
|
1418
|
+
}));
|
|
1277
1419
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1420
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1278
1421
|
for (const [index, cached] of cachedChunks) {
|
|
1279
|
-
if (index
|
|
1422
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
1423
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1424
|
+
results[index] = cached;
|
|
1425
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1426
|
+
} else invalidCachedIndices.add(index);
|
|
1280
1427
|
}
|
|
1281
1428
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1282
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1429
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1430
|
+
const jobStartedAt = Date.now();
|
|
1431
|
+
const jobDeadlineAt = options.timeouts?.totalJobMs !== void 0 && options.timeouts.totalJobMs > 0 ? jobStartedAt + options.timeouts.totalJobMs : void 0;
|
|
1283
1432
|
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1284
1433
|
fallbackJobScope = jobScope;
|
|
1285
1434
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
@@ -1292,7 +1441,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1292
1441
|
const index = nextIndex++;
|
|
1293
1442
|
if (index >= chunks.length) return;
|
|
1294
1443
|
if (!shouldSynthesize(index)) continue;
|
|
1295
|
-
if (
|
|
1444
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1445
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1296
1448
|
const chunk = chunks[index];
|
|
1297
1449
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1298
1450
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -1325,7 +1477,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1325
1477
|
retryAttempt,
|
|
1326
1478
|
nextRetryDelayMs,
|
|
1327
1479
|
isRetrying: true
|
|
1328
|
-
})
|
|
1480
|
+
}),
|
|
1481
|
+
jobDeadlineAt
|
|
1329
1482
|
);
|
|
1330
1483
|
} catch (error) {
|
|
1331
1484
|
if (chunkScope?.timedOut())
|
|
@@ -1377,6 +1530,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1377
1530
|
}))
|
|
1378
1531
|
} : {}
|
|
1379
1532
|
};
|
|
1533
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1380
1534
|
completed += 1;
|
|
1381
1535
|
options.onProgress?.({
|
|
1382
1536
|
currentChunk: completed,
|
|
@@ -1388,7 +1542,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1388
1542
|
durationMs: Date.now() - startedAt
|
|
1389
1543
|
});
|
|
1390
1544
|
} catch (error) {
|
|
1391
|
-
|
|
1545
|
+
const wasCancelled = firstError !== void 0 || Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1546
|
+
firstError ?? (firstError = error);
|
|
1547
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
1548
|
+
chunkStates[index] = {
|
|
1549
|
+
chunkIndex: index,
|
|
1550
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
1551
|
+
isOriginalFailure: !wasCancelled,
|
|
1552
|
+
canResume: true,
|
|
1553
|
+
error
|
|
1554
|
+
};
|
|
1392
1555
|
options.onProgress?.({
|
|
1393
1556
|
currentChunk: completed,
|
|
1394
1557
|
totalChunks: chunks.length,
|
|
@@ -1400,19 +1563,32 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
1400
1563
|
error
|
|
1401
1564
|
});
|
|
1402
1565
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1403
|
-
firstError ?? (firstError = error);
|
|
1404
1566
|
return;
|
|
1405
1567
|
}
|
|
1406
1568
|
}
|
|
1407
1569
|
};
|
|
1408
1570
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1571
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
1572
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1573
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1574
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1409
1578
|
if (failedIndices.size > 0) {
|
|
1410
1579
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1580
|
+
const synthesizedChunks = results.flatMap(
|
|
1581
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1582
|
+
);
|
|
1411
1583
|
error.partialResult = {
|
|
1412
|
-
synthesizedChunks
|
|
1413
|
-
completedChunks:
|
|
1414
|
-
pendingChunkIndices:
|
|
1584
|
+
synthesizedChunks,
|
|
1585
|
+
completedChunks: synthesizedChunks,
|
|
1586
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
1587
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1588
|
+
),
|
|
1415
1589
|
failedChunkIndices: [...failedIndices],
|
|
1590
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1591
|
+
chunkStates,
|
|
1416
1592
|
totalChunks: chunks.length
|
|
1417
1593
|
};
|
|
1418
1594
|
throw error;
|
|
@@ -1458,7 +1634,16 @@ var AzureTtsClient = class {
|
|
|
1458
1634
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
1459
1635
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1460
1636
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1461
|
-
const config = {
|
|
1637
|
+
const config = {
|
|
1638
|
+
endpoint,
|
|
1639
|
+
region,
|
|
1640
|
+
subscriptionKey,
|
|
1641
|
+
outputFormat,
|
|
1642
|
+
signal,
|
|
1643
|
+
timeoutMs,
|
|
1644
|
+
timeouts,
|
|
1645
|
+
retryOptions: __privateGet(this, _options).retryOptions
|
|
1646
|
+
};
|
|
1462
1647
|
return synthesizeSpeech(ssml, config);
|
|
1463
1648
|
}
|
|
1464
1649
|
async synthesizeSsml(ssml, options = {}) {
|
|
@@ -1475,7 +1660,13 @@ var AzureTtsClient = class {
|
|
|
1475
1660
|
timeouts: options.timeouts ?? timeouts,
|
|
1476
1661
|
sourceNodePath: options.sourceNodePath,
|
|
1477
1662
|
sourceTextSegments: options.sourceTextSegments,
|
|
1478
|
-
sourceMarkers: options.sourceMarkers
|
|
1663
|
+
sourceMarkers: options.sourceMarkers,
|
|
1664
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1665
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1666
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1667
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1668
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1669
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
1479
1670
|
});
|
|
1480
1671
|
}
|
|
1481
1672
|
async synthesizeChunks(chunks, options = {}) {
|
|
@@ -1493,12 +1684,13 @@ var AzureTtsClient = class {
|
|
|
1493
1684
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1494
1685
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1495
1686
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1496
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
1687
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
1497
1688
|
resumeChunks: options.resumeChunks,
|
|
1498
1689
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
1499
|
-
customMerger: options.customMerger,
|
|
1500
|
-
outputMimeType: options.outputMimeType,
|
|
1501
|
-
postMergeValidator: options.postMergeValidator
|
|
1690
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
1691
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
1692
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
1693
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
1502
1694
|
});
|
|
1503
1695
|
}
|
|
1504
1696
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -1626,6 +1818,7 @@ export {
|
|
|
1626
1818
|
buildPartialSsml,
|
|
1627
1819
|
buildSsml,
|
|
1628
1820
|
canMergeAudioFormat,
|
|
1821
|
+
computeChunkFingerprint,
|
|
1629
1822
|
createAzureUrlValidatorRunner,
|
|
1630
1823
|
extractSsmlText,
|
|
1631
1824
|
extractSsmlTranslatableText,
|