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.js
CHANGED
|
@@ -52,6 +52,7 @@ __export(src_exports, {
|
|
|
52
52
|
buildPartialSsml: () => buildPartialSsml,
|
|
53
53
|
buildSsml: () => buildSsml,
|
|
54
54
|
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
55
|
+
computeChunkFingerprint: () => computeChunkFingerprint,
|
|
55
56
|
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
56
57
|
extractSsmlText: () => extractSsmlText,
|
|
57
58
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
@@ -4583,6 +4584,9 @@ var OUTPUT_FORMATS = {
|
|
|
4583
4584
|
function resolveMimeType(outputFormat) {
|
|
4584
4585
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
4585
4586
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
4587
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
4588
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
4589
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
4586
4590
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
4587
4591
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
4588
4592
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -4611,6 +4615,27 @@ function createSpeechConfig(config) {
|
|
|
4611
4615
|
}
|
|
4612
4616
|
|
|
4613
4617
|
// packages/azure-tts-client/src/synthesis.ts
|
|
4618
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT) {
|
|
4619
|
+
const readAttribute3 = (name) => {
|
|
4620
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
4621
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
4622
|
+
};
|
|
4623
|
+
const payload = JSON.stringify({
|
|
4624
|
+
ssml,
|
|
4625
|
+
outputFormat,
|
|
4626
|
+
voice: readAttribute3("(?:name|voice)"),
|
|
4627
|
+
language: readAttribute3("(?:xml:lang|lang)"),
|
|
4628
|
+
rate: readAttribute3("rate"),
|
|
4629
|
+
pitch: readAttribute3("pitch")
|
|
4630
|
+
});
|
|
4631
|
+
let hash = 0xcbf29ce484222325n;
|
|
4632
|
+
const mask = 0xffffffffffffffffn;
|
|
4633
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
4634
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
4635
|
+
hash = hash * 0x100000001b3n & mask;
|
|
4636
|
+
}
|
|
4637
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
4638
|
+
}
|
|
4614
4639
|
function ascii(bytes, offset, value) {
|
|
4615
4640
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
4616
4641
|
}
|
|
@@ -4650,9 +4675,11 @@ function parseWav(buffer) {
|
|
|
4650
4675
|
}
|
|
4651
4676
|
return { chunks, data, format };
|
|
4652
4677
|
}
|
|
4653
|
-
function
|
|
4654
|
-
const match =
|
|
4655
|
-
|
|
4678
|
+
function formatSampleRate(format) {
|
|
4679
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
4680
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
4681
|
+
const value = Number(match[1]);
|
|
4682
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
4656
4683
|
}
|
|
4657
4684
|
function formatChannels(format, fallback) {
|
|
4658
4685
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -4660,11 +4687,11 @@ function formatChannels(format, fallback) {
|
|
|
4660
4687
|
return fallback;
|
|
4661
4688
|
}
|
|
4662
4689
|
function formatAudioSpecification(format) {
|
|
4663
|
-
const sampleRate =
|
|
4690
|
+
const sampleRate = formatSampleRate(format);
|
|
4664
4691
|
const channels = formatChannels(format, 0);
|
|
4665
4692
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
4666
4693
|
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
4667
|
-
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /
|
|
4694
|
+
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";
|
|
4668
4695
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
4669
4696
|
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;
|
|
4670
4697
|
return {
|
|
@@ -4677,7 +4704,7 @@ function formatAudioSpecification(format) {
|
|
|
4677
4704
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
4678
4705
|
...container ? { container } : {},
|
|
4679
4706
|
isVbr: /vbr/i.test(format),
|
|
4680
|
-
isCompressed: codec
|
|
4707
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
4681
4708
|
};
|
|
4682
4709
|
}
|
|
4683
4710
|
function parseMp3Specification(buffer, format) {
|
|
@@ -4728,27 +4755,43 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
4728
4755
|
const channels = view.getUint16(2, true);
|
|
4729
4756
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
4730
4757
|
const formatCode = view.getUint16(0, true);
|
|
4758
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
4759
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
4731
4760
|
return {
|
|
4732
4761
|
format,
|
|
4733
4762
|
mimeType: "audio/wav",
|
|
4734
|
-
codec
|
|
4763
|
+
codec,
|
|
4735
4764
|
sampleRate,
|
|
4736
4765
|
channels,
|
|
4737
4766
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
4738
4767
|
bitDepth: bitsPerSample,
|
|
4739
4768
|
container: "riff-wave",
|
|
4740
4769
|
isVbr: false,
|
|
4741
|
-
isCompressed:
|
|
4770
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
4742
4771
|
};
|
|
4743
4772
|
}
|
|
4744
4773
|
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
4745
|
-
|
|
4774
|
+
const specification = formatAudioSpecification(format);
|
|
4775
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
4776
|
+
return specification;
|
|
4777
|
+
}
|
|
4778
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
4779
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
4780
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
4781
|
+
}
|
|
4782
|
+
if (specification.codec === "siren" || specification.codec === "silk") return;
|
|
4783
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
4784
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
4785
|
+
throw new Error(
|
|
4786
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
4787
|
+
);
|
|
4788
|
+
}
|
|
4746
4789
|
}
|
|
4747
4790
|
function validateAudioSpecifications(specs) {
|
|
4748
4791
|
const first = specs[0];
|
|
4749
4792
|
if (!first) return;
|
|
4750
4793
|
const mismatch = specs.find(
|
|
4751
|
-
(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
|
|
4794
|
+
(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
|
|
4752
4795
|
);
|
|
4753
4796
|
if (mismatch)
|
|
4754
4797
|
throw new AudioFormatMismatchError(
|
|
@@ -4831,6 +4874,30 @@ function isWavFormat(format) {
|
|
|
4831
4874
|
function isRawFormat(format) {
|
|
4832
4875
|
return /^raw(?:-|$)/i.test(format);
|
|
4833
4876
|
}
|
|
4877
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType) {
|
|
4878
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
4879
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
4880
|
+
}
|
|
4881
|
+
const specification = inspectAudioSpecification(merged, format);
|
|
4882
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
4883
|
+
const firstInput = inputSpecs[0];
|
|
4884
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
4885
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
4886
|
+
...inputSpecs,
|
|
4887
|
+
specification
|
|
4888
|
+
]);
|
|
4889
|
+
}
|
|
4890
|
+
if (isRawFormat(format)) {
|
|
4891
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
4892
|
+
if (merged.byteLength !== expectedSize) {
|
|
4893
|
+
throw new MergeError(
|
|
4894
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
4895
|
+
);
|
|
4896
|
+
}
|
|
4897
|
+
validateRawAudioBuffer(merged, specification);
|
|
4898
|
+
}
|
|
4899
|
+
return specification;
|
|
4900
|
+
}
|
|
4834
4901
|
function resolveMergeAudioFormat(format) {
|
|
4835
4902
|
if (isWavFormat(format)) return "wav";
|
|
4836
4903
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -4883,7 +4950,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
4883
4950
|
}
|
|
4884
4951
|
}
|
|
4885
4952
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
4886
|
-
async function
|
|
4953
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
4887
4954
|
if (config.signal?.aborted) {
|
|
4888
4955
|
throw new SynthesisCancelledError();
|
|
4889
4956
|
}
|
|
@@ -5013,6 +5080,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5013
5080
|
rejectWithError(err);
|
|
5014
5081
|
return;
|
|
5015
5082
|
}
|
|
5083
|
+
let audioSpec;
|
|
5084
|
+
try {
|
|
5085
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
5086
|
+
} catch (error) {
|
|
5087
|
+
rejectWithError(error);
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5016
5090
|
settled = true;
|
|
5017
5091
|
cleanup();
|
|
5018
5092
|
closeResources();
|
|
@@ -5048,8 +5122,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5048
5122
|
resolve({
|
|
5049
5123
|
audioData: result.audioData,
|
|
5050
5124
|
durationMs,
|
|
5051
|
-
audioSpec
|
|
5052
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
5125
|
+
audioSpec,
|
|
5126
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
5053
5127
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
5054
5128
|
...requestId ? { requestId } : {},
|
|
5055
5129
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -5062,7 +5136,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5062
5136
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
5063
5137
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
5064
5138
|
}
|
|
5065
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
5139
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
5066
5140
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
5067
5141
|
timeout = setTimeout(
|
|
5068
5142
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -5115,7 +5189,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
5115
5189
|
}
|
|
5116
5190
|
});
|
|
5117
5191
|
}
|
|
5118
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
5192
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
5119
5193
|
const options = retryOptions ? {
|
|
5120
5194
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
5121
5195
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -5126,17 +5200,29 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
5126
5200
|
while (true) {
|
|
5127
5201
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
5128
5202
|
try {
|
|
5129
|
-
return await
|
|
5203
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
5130
5204
|
} catch (error) {
|
|
5131
5205
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
5132
5206
|
throw error;
|
|
5133
5207
|
attempt += 1;
|
|
5134
5208
|
const delayMs = retryDelay(options, attempt, error);
|
|
5209
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
5210
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
5211
|
+
throw new SynthesisTimeoutError(
|
|
5212
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5135
5215
|
onRetry(attempt, delayMs);
|
|
5136
|
-
await waitForRetry(delayMs, config.signal);
|
|
5216
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
5137
5217
|
}
|
|
5138
5218
|
}
|
|
5139
5219
|
}
|
|
5220
|
+
async function synthesizeSsml(ssml, config) {
|
|
5221
|
+
const totalJobMs = config.timeouts?.totalJobMs;
|
|
5222
|
+
const deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
5223
|
+
if (!config.retryOptions) return synthesizeSsmlOnce(ssml, config);
|
|
5224
|
+
return synthesizeWithRetry(ssml, config, config.retryOptions, () => void 0, deadlineAtMs);
|
|
5225
|
+
}
|
|
5140
5226
|
function createAbortScope(parent, timeoutMs) {
|
|
5141
5227
|
const controller = new AbortController();
|
|
5142
5228
|
let didTimeout = false;
|
|
@@ -5157,10 +5243,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
5157
5243
|
abort: () => controller.abort()
|
|
5158
5244
|
};
|
|
5159
5245
|
}
|
|
5160
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
5246
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
5161
5247
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
5162
5248
|
try {
|
|
5163
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
5249
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
5164
5250
|
} catch (error) {
|
|
5165
5251
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
5166
5252
|
throw error;
|
|
@@ -5169,18 +5255,36 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
5169
5255
|
}
|
|
5170
5256
|
}
|
|
5171
5257
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
5172
|
-
const results = new Array(chunks.length);
|
|
5173
5258
|
const totalChunks = chunks.length;
|
|
5259
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
5260
|
+
const fingerprints = inputs.map(
|
|
5261
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT)
|
|
5262
|
+
);
|
|
5263
|
+
const results = new Array(totalChunks);
|
|
5174
5264
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
5265
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
5266
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
5267
|
+
chunkIndex,
|
|
5268
|
+
status: "pending",
|
|
5269
|
+
canResume: true
|
|
5270
|
+
}));
|
|
5175
5271
|
for (const [index, cached] of cachedChunks) {
|
|
5176
|
-
if (index
|
|
5272
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
5273
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
5274
|
+
if (isValid) {
|
|
5275
|
+
results[index] = { ...cached };
|
|
5276
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
5277
|
+
} else {
|
|
5278
|
+
invalidCachedIndices.add(index);
|
|
5279
|
+
}
|
|
5177
5280
|
}
|
|
5178
5281
|
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
5179
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
5282
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
5283
|
+
const jobStartedAt = Date.now();
|
|
5284
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
5180
5285
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
5181
5286
|
const report = (event) => config.onProgress?.(event);
|
|
5182
|
-
for (const [index,
|
|
5183
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5287
|
+
for (const [index, input] of inputs.entries()) {
|
|
5184
5288
|
report({
|
|
5185
5289
|
currentChunk: index,
|
|
5186
5290
|
totalChunks,
|
|
@@ -5202,8 +5306,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5202
5306
|
if (index >= chunks.length) return;
|
|
5203
5307
|
if (!shouldSynthesize(index)) continue;
|
|
5204
5308
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
5205
|
-
const
|
|
5206
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5309
|
+
const input = inputs[index];
|
|
5207
5310
|
report({
|
|
5208
5311
|
currentChunk: completed,
|
|
5209
5312
|
totalChunks,
|
|
@@ -5240,9 +5343,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5240
5343
|
retryAttempt,
|
|
5241
5344
|
nextRetryDelayMs,
|
|
5242
5345
|
isRetrying: true
|
|
5243
|
-
})
|
|
5346
|
+
}),
|
|
5347
|
+
jobDeadlineAt
|
|
5244
5348
|
);
|
|
5245
5349
|
results[index] = result;
|
|
5350
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
5246
5351
|
completed += 1;
|
|
5247
5352
|
report({
|
|
5248
5353
|
currentChunk: completed,
|
|
@@ -5254,7 +5359,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5254
5359
|
durationMs: Date.now() - startedAt
|
|
5255
5360
|
});
|
|
5256
5361
|
} catch (error) {
|
|
5257
|
-
|
|
5362
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
5363
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
5364
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
5365
|
+
chunkStates[index] = {
|
|
5366
|
+
chunkIndex: index,
|
|
5367
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
5368
|
+
isOriginalFailure: !wasCancelled,
|
|
5369
|
+
canResume: true,
|
|
5370
|
+
error
|
|
5371
|
+
};
|
|
5258
5372
|
report({
|
|
5259
5373
|
currentChunk: completed,
|
|
5260
5374
|
totalChunks,
|
|
@@ -5265,7 +5379,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5265
5379
|
durationMs: Date.now() - startedAt,
|
|
5266
5380
|
error
|
|
5267
5381
|
});
|
|
5268
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
5269
5382
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
5270
5383
|
return;
|
|
5271
5384
|
}
|
|
@@ -5283,11 +5396,25 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5283
5396
|
postMergeValidator: config.postMergeValidator
|
|
5284
5397
|
});
|
|
5285
5398
|
} catch (error) {
|
|
5399
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
5400
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
5401
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
5402
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
const synthesizedChunks = results.flatMap(
|
|
5407
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
5408
|
+
);
|
|
5286
5409
|
const partial = {
|
|
5287
|
-
synthesizedChunks
|
|
5288
|
-
completedChunks:
|
|
5289
|
-
pendingChunkIndices:
|
|
5410
|
+
synthesizedChunks,
|
|
5411
|
+
completedChunks: synthesizedChunks,
|
|
5412
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
5413
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
5414
|
+
),
|
|
5290
5415
|
failedChunkIndices: [...failedIndices],
|
|
5416
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
5417
|
+
chunkStates,
|
|
5291
5418
|
totalChunks
|
|
5292
5419
|
};
|
|
5293
5420
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
@@ -5385,16 +5512,15 @@ function mergeSynthesisResults(results, options) {
|
|
|
5385
5512
|
})
|
|
5386
5513
|
).then((merged) => {
|
|
5387
5514
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
5388
|
-
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
5389
|
-
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
5390
5515
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
5391
|
-
const
|
|
5392
|
-
results,
|
|
5516
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
5393
5517
|
merged,
|
|
5394
5518
|
format,
|
|
5395
|
-
|
|
5396
|
-
|
|
5519
|
+
buffers,
|
|
5520
|
+
inputSpecs,
|
|
5521
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format)
|
|
5397
5522
|
);
|
|
5523
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
5398
5524
|
return Promise.resolve(
|
|
5399
5525
|
resolvedOptions.postMergeValidator?.(result, {
|
|
5400
5526
|
format,
|
|
@@ -5523,7 +5649,7 @@ function resolveConcurrency2(value, total) {
|
|
|
5523
5649
|
if (value === Infinity) return Math.max(1, total);
|
|
5524
5650
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
5525
5651
|
}
|
|
5526
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
5652
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
5527
5653
|
const retry = options ? {
|
|
5528
5654
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
5529
5655
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -5540,6 +5666,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
5540
5666
|
throw error;
|
|
5541
5667
|
attempt += 1;
|
|
5542
5668
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
5669
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
5670
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
5671
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
5672
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
5673
|
+
}
|
|
5543
5674
|
onRetry(attempt, delayMs);
|
|
5544
5675
|
if (delayMs > 0)
|
|
5545
5676
|
await new Promise((resolve, reject) => {
|
|
@@ -5587,20 +5718,24 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
5587
5718
|
diagnostics: errors
|
|
5588
5719
|
});
|
|
5589
5720
|
}
|
|
5721
|
+
const jobScope = options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts.totalJobMs) : void 0;
|
|
5590
5722
|
try {
|
|
5591
5723
|
return {
|
|
5592
5724
|
ok: true,
|
|
5593
5725
|
success: true,
|
|
5594
5726
|
status: "success",
|
|
5595
5727
|
value: await client.synthesizeSsml(ssml, {
|
|
5596
|
-
signal: options.signal,
|
|
5728
|
+
signal: jobScope?.signal ?? options.signal,
|
|
5597
5729
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
5598
5730
|
timeouts: options.timeouts
|
|
5599
5731
|
})
|
|
5600
5732
|
};
|
|
5601
5733
|
} catch (error) {
|
|
5734
|
+
if (jobScope?.timedOut()) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
5602
5735
|
const synthesisError = toSynthesisError(error);
|
|
5603
5736
|
return failure(synthesisError);
|
|
5737
|
+
} finally {
|
|
5738
|
+
jobScope?.dispose();
|
|
5604
5739
|
}
|
|
5605
5740
|
}
|
|
5606
5741
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
@@ -5672,17 +5807,32 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5672
5807
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
5673
5808
|
customMerger: options.customMerger,
|
|
5674
5809
|
outputMimeType: options.outputMimeType,
|
|
5675
|
-
postMergeValidator: options.postMergeValidator
|
|
5810
|
+
postMergeValidator: options.postMergeValidator,
|
|
5811
|
+
resumeValidation: options.resumeValidation
|
|
5676
5812
|
});
|
|
5677
5813
|
return { ok: true, success: true, status: "success", value };
|
|
5678
5814
|
}
|
|
5815
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
5816
|
+
const fingerprints = inputs.map((chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat));
|
|
5679
5817
|
const results = new Array(chunks.length);
|
|
5818
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
5819
|
+
chunkIndex,
|
|
5820
|
+
status: "pending",
|
|
5821
|
+
canResume: true
|
|
5822
|
+
}));
|
|
5680
5823
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
5824
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
5681
5825
|
for (const [index, cached] of cachedChunks) {
|
|
5682
|
-
if (index
|
|
5826
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
5827
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
5828
|
+
results[index] = cached;
|
|
5829
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
5830
|
+
} else invalidCachedIndices.add(index);
|
|
5683
5831
|
}
|
|
5684
5832
|
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
5685
|
-
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
5833
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
5834
|
+
const jobStartedAt = Date.now();
|
|
5835
|
+
const jobDeadlineAt = options.timeouts?.totalJobMs !== void 0 && options.timeouts.totalJobMs > 0 ? jobStartedAt + options.timeouts.totalJobMs : void 0;
|
|
5686
5836
|
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
5687
5837
|
fallbackJobScope = jobScope;
|
|
5688
5838
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
@@ -5695,7 +5845,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5695
5845
|
const index = nextIndex++;
|
|
5696
5846
|
if (index >= chunks.length) return;
|
|
5697
5847
|
if (!shouldSynthesize(index)) continue;
|
|
5698
|
-
if (
|
|
5848
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
5849
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5699
5852
|
const chunk = chunks[index];
|
|
5700
5853
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5701
5854
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -5728,7 +5881,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5728
5881
|
retryAttempt,
|
|
5729
5882
|
nextRetryDelayMs,
|
|
5730
5883
|
isRetrying: true
|
|
5731
|
-
})
|
|
5884
|
+
}),
|
|
5885
|
+
jobDeadlineAt
|
|
5732
5886
|
);
|
|
5733
5887
|
} catch (error) {
|
|
5734
5888
|
if (chunkScope?.timedOut())
|
|
@@ -5780,6 +5934,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5780
5934
|
}))
|
|
5781
5935
|
} : {}
|
|
5782
5936
|
};
|
|
5937
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
5783
5938
|
completed += 1;
|
|
5784
5939
|
options.onProgress?.({
|
|
5785
5940
|
currentChunk: completed,
|
|
@@ -5791,7 +5946,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5791
5946
|
durationMs: Date.now() - startedAt
|
|
5792
5947
|
});
|
|
5793
5948
|
} catch (error) {
|
|
5794
|
-
|
|
5949
|
+
const wasCancelled = firstError !== void 0 || Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
5950
|
+
firstError ?? (firstError = error);
|
|
5951
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
5952
|
+
chunkStates[index] = {
|
|
5953
|
+
chunkIndex: index,
|
|
5954
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
5955
|
+
isOriginalFailure: !wasCancelled,
|
|
5956
|
+
canResume: true,
|
|
5957
|
+
error
|
|
5958
|
+
};
|
|
5795
5959
|
options.onProgress?.({
|
|
5796
5960
|
currentChunk: completed,
|
|
5797
5961
|
totalChunks: chunks.length,
|
|
@@ -5803,19 +5967,32 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5803
5967
|
error
|
|
5804
5968
|
});
|
|
5805
5969
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
5806
|
-
firstError ?? (firstError = error);
|
|
5807
5970
|
return;
|
|
5808
5971
|
}
|
|
5809
5972
|
}
|
|
5810
5973
|
};
|
|
5811
5974
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
5975
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
5976
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
5977
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
5978
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5812
5982
|
if (failedIndices.size > 0) {
|
|
5813
5983
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
5984
|
+
const synthesizedChunks = results.flatMap(
|
|
5985
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
5986
|
+
);
|
|
5814
5987
|
error.partialResult = {
|
|
5815
|
-
synthesizedChunks
|
|
5816
|
-
completedChunks:
|
|
5817
|
-
pendingChunkIndices:
|
|
5988
|
+
synthesizedChunks,
|
|
5989
|
+
completedChunks: synthesizedChunks,
|
|
5990
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
5991
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
5992
|
+
),
|
|
5818
5993
|
failedChunkIndices: [...failedIndices],
|
|
5994
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
5995
|
+
chunkStates,
|
|
5819
5996
|
totalChunks: chunks.length
|
|
5820
5997
|
};
|
|
5821
5998
|
throw error;
|
|
@@ -5861,7 +6038,16 @@ var AzureTtsClient = class {
|
|
|
5861
6038
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
5862
6039
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
5863
6040
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
5864
|
-
const config = {
|
|
6041
|
+
const config = {
|
|
6042
|
+
endpoint,
|
|
6043
|
+
region,
|
|
6044
|
+
subscriptionKey,
|
|
6045
|
+
outputFormat,
|
|
6046
|
+
signal,
|
|
6047
|
+
timeoutMs,
|
|
6048
|
+
timeouts,
|
|
6049
|
+
retryOptions: __privateGet(this, _options).retryOptions
|
|
6050
|
+
};
|
|
5865
6051
|
return synthesizeSpeech(ssml, config);
|
|
5866
6052
|
}
|
|
5867
6053
|
async synthesizeSsml(ssml, options = {}) {
|
|
@@ -5878,7 +6064,13 @@ var AzureTtsClient = class {
|
|
|
5878
6064
|
timeouts: options.timeouts ?? timeouts,
|
|
5879
6065
|
sourceNodePath: options.sourceNodePath,
|
|
5880
6066
|
sourceTextSegments: options.sourceTextSegments,
|
|
5881
|
-
sourceMarkers: options.sourceMarkers
|
|
6067
|
+
sourceMarkers: options.sourceMarkers,
|
|
6068
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
6069
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
6070
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
6071
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
6072
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
6073
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
5882
6074
|
});
|
|
5883
6075
|
}
|
|
5884
6076
|
async synthesizeChunks(chunks, options = {}) {
|
|
@@ -5896,12 +6088,13 @@ var AzureTtsClient = class {
|
|
|
5896
6088
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
5897
6089
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
5898
6090
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
5899
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
6091
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
5900
6092
|
resumeChunks: options.resumeChunks,
|
|
5901
6093
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
5902
|
-
customMerger: options.customMerger,
|
|
5903
|
-
outputMimeType: options.outputMimeType,
|
|
5904
|
-
postMergeValidator: options.postMergeValidator
|
|
6094
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
6095
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
6096
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
6097
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation
|
|
5905
6098
|
});
|
|
5906
6099
|
}
|
|
5907
6100
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -6030,6 +6223,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
6030
6223
|
buildPartialSsml,
|
|
6031
6224
|
buildSsml,
|
|
6032
6225
|
canMergeAudioFormat,
|
|
6226
|
+
computeChunkFingerprint,
|
|
6033
6227
|
createAzureUrlValidatorRunner,
|
|
6034
6228
|
extractSsmlText,
|
|
6035
6229
|
extractSsmlTranslatableText,
|