ssml-builder-js 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{index.d-tpKoP1jl.d.mts → index.d-4kqVuH29.d.mts} +1 -1
- package/dist/{index.d-tpKoP1jl.d.ts → index.d-4kqVuH29.d.ts} +1 -1
- package/dist/index.d.mts +124 -44
- package/dist/index.d.ts +124 -44
- package/dist/index.js +674 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +670 -90
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +45 -24
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +45 -24
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ __export(src_exports, {
|
|
|
44
44
|
BatchChunkValidationError: () => BatchChunkValidationError,
|
|
45
45
|
ChunkValidationError: () => ChunkValidationError,
|
|
46
46
|
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
47
|
+
DeadlineController: () => DeadlineController,
|
|
48
|
+
IncompleteChunkSetError: () => IncompleteChunkSetError,
|
|
47
49
|
MergeError: () => MergeError,
|
|
48
50
|
SynthesisCancelledError: () => SynthesisCancelledError,
|
|
49
51
|
SynthesisTimeoutError: () => SynthesisTimeoutError,
|
|
@@ -52,6 +54,7 @@ __export(src_exports, {
|
|
|
52
54
|
buildPartialSsml: () => buildPartialSsml,
|
|
53
55
|
buildSsml: () => buildSsml,
|
|
54
56
|
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
57
|
+
computeChunkFingerprint: () => computeChunkFingerprint,
|
|
55
58
|
createAzureUrlValidatorRunner: () => createAzureUrlValidatorRunner,
|
|
56
59
|
extractSsmlText: () => extractSsmlText,
|
|
57
60
|
extractSsmlTranslatableText: () => extractSsmlTranslatableText,
|
|
@@ -70,6 +73,7 @@ __export(src_exports, {
|
|
|
70
73
|
parseSsml: () => parseSsml,
|
|
71
74
|
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
72
75
|
resolveMimeType: () => resolveMimeType,
|
|
76
|
+
serializeChunkError: () => serializeChunkError,
|
|
73
77
|
splitSsmlDocument: () => splitSsmlDocument,
|
|
74
78
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
75
79
|
synthesizeSsml: () => synthesizeSsml,
|
|
@@ -2802,6 +2806,15 @@ var SynthesisTimeoutError = class extends Error {
|
|
|
2802
2806
|
this.name = "SynthesisTimeoutError";
|
|
2803
2807
|
}
|
|
2804
2808
|
};
|
|
2809
|
+
var IncompleteChunkSetError = class extends Error {
|
|
2810
|
+
constructor(totalChunks, missingChunkIndices) {
|
|
2811
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
2812
|
+
this.kind = "incomplete-chunk-set";
|
|
2813
|
+
this.name = "IncompleteChunkSetError";
|
|
2814
|
+
this.totalChunks = totalChunks;
|
|
2815
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2805
2818
|
var MergeError = class extends Error {
|
|
2806
2819
|
constructor(message, cause) {
|
|
2807
2820
|
super(message);
|
|
@@ -2826,8 +2839,32 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
2826
2839
|
this.format = format;
|
|
2827
2840
|
}
|
|
2828
2841
|
};
|
|
2842
|
+
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
2843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2844
|
+
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
2845
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
2846
|
+
const code = kind === "validation-error" ? "VALIDATION_ERROR" : kind === "timeout" || /tim(?:e|ed) ?out|deadline/i.test(message) ? "TIMEOUT" : kind === "cancelled" || /cancel|abort/i.test(message) ? "CANCELLED" : kind === "audio-format-mismatch" || kind === "unsupported-format-error" ? "FORMAT_MISMATCH" : kind === "merge-error" || phase === "merge" ? "MERGE_ERROR" : "AZURE_API_ERROR";
|
|
2847
|
+
const details = {};
|
|
2848
|
+
if (error instanceof AzureTtsError) {
|
|
2849
|
+
details.statusText = error.statusText;
|
|
2850
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
2851
|
+
}
|
|
2852
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
2853
|
+
details.totalChunks = error.totalChunks;
|
|
2854
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
code,
|
|
2858
|
+
phase,
|
|
2859
|
+
message,
|
|
2860
|
+
isOriginalFailure,
|
|
2861
|
+
isRetryable: code === "AZURE_API_ERROR" && (status === 429 || status !== void 0 && status >= 500 || /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message)),
|
|
2862
|
+
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
2863
|
+
...Object.keys(details).length > 0 ? { details } : {}
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2829
2866
|
function toSynthesisError(error) {
|
|
2830
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
2867
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
2831
2868
|
return error;
|
|
2832
2869
|
const message = error instanceof Error ? error.message : String(error);
|
|
2833
2870
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -2839,6 +2876,55 @@ function createSpeechSdkError(error) {
|
|
|
2839
2876
|
return new AzureTtsSdkError(message);
|
|
2840
2877
|
}
|
|
2841
2878
|
|
|
2879
|
+
// packages/azure-tts-client/src/deadline.ts
|
|
2880
|
+
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
2881
|
+
var DeadlineController = class {
|
|
2882
|
+
constructor(totalJobMs, parent) {
|
|
2883
|
+
__privateAdd(this, _controller, new AbortController());
|
|
2884
|
+
__privateAdd(this, _parent);
|
|
2885
|
+
__privateAdd(this, _onParentAbort);
|
|
2886
|
+
__privateAdd(this, _timer);
|
|
2887
|
+
__privateAdd(this, _timedOut, false);
|
|
2888
|
+
__privateSet(this, _parent, parent);
|
|
2889
|
+
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
2890
|
+
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
2891
|
+
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
2892
|
+
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
2893
|
+
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
2894
|
+
if (this.deadlineAtMs !== void 0) {
|
|
2895
|
+
__privateSet(this, _timer, setTimeout(
|
|
2896
|
+
() => {
|
|
2897
|
+
__privateSet(this, _timedOut, true);
|
|
2898
|
+
__privateGet(this, _controller).abort();
|
|
2899
|
+
},
|
|
2900
|
+
Math.max(0, this.deadlineAtMs - Date.now())
|
|
2901
|
+
));
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
get timedOut() {
|
|
2905
|
+
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
2906
|
+
}
|
|
2907
|
+
get remainingMs() {
|
|
2908
|
+
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
2909
|
+
}
|
|
2910
|
+
throwIfExpired() {
|
|
2911
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
2912
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
2913
|
+
}
|
|
2914
|
+
abort() {
|
|
2915
|
+
__privateGet(this, _controller).abort();
|
|
2916
|
+
}
|
|
2917
|
+
dispose() {
|
|
2918
|
+
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
2919
|
+
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
_controller = new WeakMap();
|
|
2923
|
+
_parent = new WeakMap();
|
|
2924
|
+
_onParentAbort = new WeakMap();
|
|
2925
|
+
_timer = new WeakMap();
|
|
2926
|
+
_timedOut = new WeakMap();
|
|
2927
|
+
|
|
2842
2928
|
// packages/azure-tts-client/src/synthesis.ts
|
|
2843
2929
|
var SpeechSDK2 = __toESM(require("microsoft-cognitiveservices-speech-sdk"));
|
|
2844
2930
|
|
|
@@ -4583,6 +4669,9 @@ var OUTPUT_FORMATS = {
|
|
|
4583
4669
|
function resolveMimeType(outputFormat) {
|
|
4584
4670
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
4585
4671
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
4672
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
4673
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
4674
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
4586
4675
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
4587
4676
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
4588
4677
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
@@ -4611,6 +4700,32 @@ function createSpeechConfig(config) {
|
|
|
4611
4700
|
}
|
|
4612
4701
|
|
|
4613
4702
|
// packages/azure-tts-client/src/synthesis.ts
|
|
4703
|
+
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
4704
|
+
const readAttribute3 = (name) => {
|
|
4705
|
+
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
4706
|
+
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
4707
|
+
};
|
|
4708
|
+
const headers = Object.fromEntries(
|
|
4709
|
+
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
4710
|
+
);
|
|
4711
|
+
const payload = JSON.stringify({
|
|
4712
|
+
ssml,
|
|
4713
|
+
outputFormat,
|
|
4714
|
+
region: options.region ?? "",
|
|
4715
|
+
endpoint: options.endpoint ?? "",
|
|
4716
|
+
voice: options.voice ?? readAttribute3("(?:name|voice)"),
|
|
4717
|
+
lang: options.lang ?? readAttribute3("(?:xml:lang|lang)"),
|
|
4718
|
+
customHeaders: headers,
|
|
4719
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
4720
|
+
});
|
|
4721
|
+
let hash = 0xcbf29ce484222325n;
|
|
4722
|
+
const mask = 0xffffffffffffffffn;
|
|
4723
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
4724
|
+
hash ^= BigInt(payload.charCodeAt(index));
|
|
4725
|
+
hash = hash * 0x100000001b3n & mask;
|
|
4726
|
+
}
|
|
4727
|
+
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
4728
|
+
}
|
|
4614
4729
|
function ascii(bytes, offset, value) {
|
|
4615
4730
|
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
4616
4731
|
}
|
|
@@ -4650,9 +4765,11 @@ function parseWav(buffer) {
|
|
|
4650
4765
|
}
|
|
4651
4766
|
return { chunks, data, format };
|
|
4652
4767
|
}
|
|
4653
|
-
function
|
|
4654
|
-
const match =
|
|
4655
|
-
|
|
4768
|
+
function formatSampleRate(format) {
|
|
4769
|
+
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
4770
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
4771
|
+
const value = Number(match[1]);
|
|
4772
|
+
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
4656
4773
|
}
|
|
4657
4774
|
function formatChannels(format, fallback) {
|
|
4658
4775
|
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
@@ -4660,11 +4777,11 @@ function formatChannels(format, fallback) {
|
|
|
4660
4777
|
return fallback;
|
|
4661
4778
|
}
|
|
4662
4779
|
function formatAudioSpecification(format) {
|
|
4663
|
-
const sampleRate =
|
|
4780
|
+
const sampleRate = formatSampleRate(format);
|
|
4664
4781
|
const channels = formatChannels(format, 0);
|
|
4665
4782
|
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
4666
4783
|
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" : /
|
|
4784
|
+
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
4785
|
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
4669
4786
|
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
4787
|
return {
|
|
@@ -4677,7 +4794,7 @@ function formatAudioSpecification(format) {
|
|
|
4677
4794
|
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
4678
4795
|
...container ? { container } : {},
|
|
4679
4796
|
isVbr: /vbr/i.test(format),
|
|
4680
|
-
isCompressed: codec
|
|
4797
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
4681
4798
|
};
|
|
4682
4799
|
}
|
|
4683
4800
|
function parseMp3Specification(buffer, format) {
|
|
@@ -4719,6 +4836,129 @@ function parseMp3Specification(buffer, format) {
|
|
|
4719
4836
|
}
|
|
4720
4837
|
return void 0;
|
|
4721
4838
|
}
|
|
4839
|
+
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
4840
|
+
const first = bytes[offset];
|
|
4841
|
+
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
4842
|
+
let mask = 128;
|
|
4843
|
+
let length = 1;
|
|
4844
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
4845
|
+
mask >>= 1;
|
|
4846
|
+
length += 1;
|
|
4847
|
+
}
|
|
4848
|
+
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
4849
|
+
let value = preserveMarker ? first : first & mask - 1;
|
|
4850
|
+
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
4851
|
+
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
4852
|
+
throw new Error("EBML unknown-size elements are not supported.");
|
|
4853
|
+
return { value, length };
|
|
4854
|
+
}
|
|
4855
|
+
function readEbmlElement(bytes, offset) {
|
|
4856
|
+
const id = readEbmlVint(bytes, offset, true);
|
|
4857
|
+
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
4858
|
+
const dataStart = offset + id.length + size.length;
|
|
4859
|
+
const dataEnd = dataStart + size.value;
|
|
4860
|
+
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
4861
|
+
return { id: id.value, dataStart, dataEnd };
|
|
4862
|
+
}
|
|
4863
|
+
function ebmlText(bytes, element) {
|
|
4864
|
+
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
4865
|
+
}
|
|
4866
|
+
function findEbmlElement(bytes, start, end, id) {
|
|
4867
|
+
let offset = start;
|
|
4868
|
+
while (offset < end) {
|
|
4869
|
+
const element = readEbmlElement(bytes, offset);
|
|
4870
|
+
if (element.id === id) return element;
|
|
4871
|
+
offset = element.dataEnd;
|
|
4872
|
+
}
|
|
4873
|
+
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
4874
|
+
return void 0;
|
|
4875
|
+
}
|
|
4876
|
+
function parseOggSpecification(buffer, format) {
|
|
4877
|
+
const bytes = new Uint8Array(buffer);
|
|
4878
|
+
let offset = 0;
|
|
4879
|
+
let firstPayload;
|
|
4880
|
+
let pages = 0;
|
|
4881
|
+
while (offset < bytes.byteLength) {
|
|
4882
|
+
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
4883
|
+
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
4884
|
+
const segmentCount = bytes[offset + 26] ?? 0;
|
|
4885
|
+
const lacingStart = offset + 27;
|
|
4886
|
+
const payloadStart = lacingStart + segmentCount;
|
|
4887
|
+
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
4888
|
+
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
4889
|
+
const pageEnd = payloadStart + payloadLength;
|
|
4890
|
+
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
4891
|
+
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
4892
|
+
offset = pageEnd;
|
|
4893
|
+
pages += 1;
|
|
4894
|
+
}
|
|
4895
|
+
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
4896
|
+
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
4897
|
+
const version = firstPayload[8];
|
|
4898
|
+
const channels = firstPayload[9] ?? 0;
|
|
4899
|
+
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
4900
|
+
12,
|
|
4901
|
+
true
|
|
4902
|
+
);
|
|
4903
|
+
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
4904
|
+
return {
|
|
4905
|
+
format,
|
|
4906
|
+
mimeType: "audio/ogg",
|
|
4907
|
+
codec: "opus",
|
|
4908
|
+
sampleRate,
|
|
4909
|
+
channels,
|
|
4910
|
+
container: "ogg",
|
|
4911
|
+
isVbr: true,
|
|
4912
|
+
isCompressed: true
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
function parseWebmSpecification(buffer, format) {
|
|
4916
|
+
const bytes = new Uint8Array(buffer);
|
|
4917
|
+
const ebml = readEbmlElement(bytes, 0);
|
|
4918
|
+
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
4919
|
+
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
4920
|
+
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
4921
|
+
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
4922
|
+
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
4923
|
+
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
4924
|
+
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
4925
|
+
let offset = tracks.dataStart;
|
|
4926
|
+
let opusTrack;
|
|
4927
|
+
while (offset < tracks.dataEnd) {
|
|
4928
|
+
const track = readEbmlElement(bytes, offset);
|
|
4929
|
+
if (track.id === 174) {
|
|
4930
|
+
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
4931
|
+
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
4932
|
+
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
4933
|
+
opusTrack = track;
|
|
4934
|
+
break;
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
offset = track.dataEnd;
|
|
4938
|
+
}
|
|
4939
|
+
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
4940
|
+
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
4941
|
+
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
4942
|
+
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
4943
|
+
const sampleRate = sampling ? new DataView(
|
|
4944
|
+
bytes.buffer,
|
|
4945
|
+
bytes.byteOffset + sampling.dataStart,
|
|
4946
|
+
sampling.dataEnd - sampling.dataStart
|
|
4947
|
+
).getFloat64(0, false) : 0;
|
|
4948
|
+
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
4949
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
4950
|
+
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
4951
|
+
return {
|
|
4952
|
+
format,
|
|
4953
|
+
mimeType: "audio/webm",
|
|
4954
|
+
codec: "opus",
|
|
4955
|
+
sampleRate: Math.round(sampleRate),
|
|
4956
|
+
channels: channelCount,
|
|
4957
|
+
container: "webm",
|
|
4958
|
+
isVbr: true,
|
|
4959
|
+
isCompressed: true
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4722
4962
|
function inspectAudioSpecification(buffer, format) {
|
|
4723
4963
|
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
4724
4964
|
const parsed = parseWav(buffer);
|
|
@@ -4728,27 +4968,78 @@ function inspectAudioSpecification(buffer, format) {
|
|
|
4728
4968
|
const channels = view.getUint16(2, true);
|
|
4729
4969
|
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
4730
4970
|
const formatCode = view.getUint16(0, true);
|
|
4971
|
+
const namedCodec = formatAudioSpecification(format).codec;
|
|
4972
|
+
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
4731
4973
|
return {
|
|
4732
4974
|
format,
|
|
4733
4975
|
mimeType: "audio/wav",
|
|
4734
|
-
codec
|
|
4976
|
+
codec,
|
|
4735
4977
|
sampleRate,
|
|
4736
4978
|
channels,
|
|
4737
4979
|
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
4738
4980
|
bitDepth: bitsPerSample,
|
|
4739
4981
|
container: "riff-wave",
|
|
4740
4982
|
isVbr: false,
|
|
4741
|
-
isCompressed:
|
|
4983
|
+
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
4742
4984
|
};
|
|
4743
4985
|
}
|
|
4744
|
-
if (isMp3Format(format))
|
|
4745
|
-
|
|
4986
|
+
if (isMp3Format(format)) {
|
|
4987
|
+
const specification2 = parseMp3Specification(buffer, format);
|
|
4988
|
+
return specification2 ?? formatAudioSpecification(format);
|
|
4989
|
+
}
|
|
4990
|
+
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
4991
|
+
const specification2 = parseOggSpecification(buffer, format);
|
|
4992
|
+
validateContainerFormat(specification2, format);
|
|
4993
|
+
return specification2;
|
|
4994
|
+
}
|
|
4995
|
+
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
4996
|
+
const specification2 = parseWebmSpecification(buffer, format);
|
|
4997
|
+
validateContainerFormat(specification2, format);
|
|
4998
|
+
return specification2;
|
|
4999
|
+
}
|
|
5000
|
+
const specification = formatAudioSpecification(format);
|
|
5001
|
+
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
5002
|
+
return specification;
|
|
5003
|
+
}
|
|
5004
|
+
function validateRawAudioBuffer(buffer, specification) {
|
|
5005
|
+
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
5006
|
+
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
5007
|
+
}
|
|
5008
|
+
if (specification.codec === "siren") return;
|
|
5009
|
+
if (specification.codec === "silk") {
|
|
5010
|
+
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
5011
|
+
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
5015
|
+
if (specification.codec === "opus") {
|
|
5016
|
+
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
5017
|
+
const frameCountCode = packetCode & 3;
|
|
5018
|
+
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
5019
|
+
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
5023
|
+
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
5024
|
+
throw new Error(
|
|
5025
|
+
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
5026
|
+
);
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
function validateContainerFormat(specification, format) {
|
|
5030
|
+
const expected = formatAudioSpecification(format);
|
|
5031
|
+
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
5032
|
+
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
5033
|
+
expected,
|
|
5034
|
+
specification
|
|
5035
|
+
]);
|
|
5036
|
+
}
|
|
4746
5037
|
}
|
|
4747
5038
|
function validateAudioSpecifications(specs) {
|
|
4748
5039
|
const first = specs[0];
|
|
4749
5040
|
if (!first) return;
|
|
4750
5041
|
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
|
|
5042
|
+
(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
5043
|
);
|
|
4753
5044
|
if (mismatch)
|
|
4754
5045
|
throw new AudioFormatMismatchError(
|
|
@@ -4825,12 +5116,67 @@ function stripMp3Tags(buffer) {
|
|
|
4825
5116
|
function isMp3Format(format) {
|
|
4826
5117
|
return /(?:mp3|mpeg)/i.test(format);
|
|
4827
5118
|
}
|
|
5119
|
+
function isOggFormat(format) {
|
|
5120
|
+
return /ogg/i.test(format);
|
|
5121
|
+
}
|
|
5122
|
+
function isWebmFormat(format) {
|
|
5123
|
+
return /webm/i.test(format);
|
|
5124
|
+
}
|
|
4828
5125
|
function isWavFormat(format) {
|
|
4829
5126
|
return /(?:wav|wave|riff)/i.test(format);
|
|
4830
5127
|
}
|
|
4831
5128
|
function isRawFormat(format) {
|
|
4832
5129
|
return /^raw(?:-|$)/i.test(format);
|
|
4833
5130
|
}
|
|
5131
|
+
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
5132
|
+
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
5133
|
+
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
5134
|
+
}
|
|
5135
|
+
let specification;
|
|
5136
|
+
try {
|
|
5137
|
+
specification = inspectAudioSpecification(merged, format);
|
|
5138
|
+
} catch (error) {
|
|
5139
|
+
if (!allowExternalContainer) throw error;
|
|
5140
|
+
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
5141
|
+
}
|
|
5142
|
+
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
5143
|
+
const firstInput = inputSpecs[0];
|
|
5144
|
+
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
5145
|
+
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
5146
|
+
...inputSpecs,
|
|
5147
|
+
specification
|
|
5148
|
+
]);
|
|
5149
|
+
}
|
|
5150
|
+
if (isRawFormat(format)) {
|
|
5151
|
+
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
5152
|
+
if (merged.byteLength !== expectedSize) {
|
|
5153
|
+
throw new MergeError(
|
|
5154
|
+
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
5155
|
+
);
|
|
5156
|
+
}
|
|
5157
|
+
validateRawAudioBuffer(merged, specification);
|
|
5158
|
+
}
|
|
5159
|
+
return specification;
|
|
5160
|
+
}
|
|
5161
|
+
async function withinDeadline(value, deadline) {
|
|
5162
|
+
if (!deadline) return value;
|
|
5163
|
+
deadline.throwIfExpired();
|
|
5164
|
+
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
5165
|
+
let timer;
|
|
5166
|
+
try {
|
|
5167
|
+
return await Promise.race([
|
|
5168
|
+
Promise.resolve(value),
|
|
5169
|
+
new Promise((_resolve, reject) => {
|
|
5170
|
+
timer = setTimeout(
|
|
5171
|
+
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
5172
|
+
deadline.remainingMs
|
|
5173
|
+
);
|
|
5174
|
+
})
|
|
5175
|
+
]);
|
|
5176
|
+
} finally {
|
|
5177
|
+
if (timer) clearTimeout(timer);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
4834
5180
|
function resolveMergeAudioFormat(format) {
|
|
4835
5181
|
if (isWavFormat(format)) return "wav";
|
|
4836
5182
|
if (isMp3Format(format)) return "mp3";
|
|
@@ -4843,6 +5189,7 @@ function canMergeAudioFormat(format) {
|
|
|
4843
5189
|
function mergeAudioBuffers(buffers, options) {
|
|
4844
5190
|
const format = typeof options === "string" ? options : options?.format;
|
|
4845
5191
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
5192
|
+
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
4846
5193
|
try {
|
|
4847
5194
|
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
4848
5195
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
@@ -4883,7 +5230,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
4883
5230
|
}
|
|
4884
5231
|
}
|
|
4885
5232
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
4886
|
-
async function
|
|
5233
|
+
async function synthesizeSsmlOnce(ssml, config) {
|
|
4887
5234
|
if (config.signal?.aborted) {
|
|
4888
5235
|
throw new SynthesisCancelledError();
|
|
4889
5236
|
}
|
|
@@ -5013,6 +5360,13 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5013
5360
|
rejectWithError(err);
|
|
5014
5361
|
return;
|
|
5015
5362
|
}
|
|
5363
|
+
let audioSpec;
|
|
5364
|
+
try {
|
|
5365
|
+
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
5366
|
+
} catch (error) {
|
|
5367
|
+
rejectWithError(error);
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5016
5370
|
settled = true;
|
|
5017
5371
|
cleanup();
|
|
5018
5372
|
closeResources();
|
|
@@ -5048,8 +5402,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5048
5402
|
resolve({
|
|
5049
5403
|
audioData: result.audioData,
|
|
5050
5404
|
durationMs,
|
|
5051
|
-
audioSpec
|
|
5052
|
-
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
5405
|
+
audioSpec,
|
|
5406
|
+
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
5053
5407
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
5054
5408
|
...requestId ? { requestId } : {},
|
|
5055
5409
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -5062,7 +5416,7 @@ async function synthesizeSsml(ssml, config) {
|
|
|
5062
5416
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
5063
5417
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
5064
5418
|
}
|
|
5065
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
5419
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
5066
5420
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
5067
5421
|
timeout = setTimeout(
|
|
5068
5422
|
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
@@ -5115,7 +5469,7 @@ async function waitForRetry(delayMs, signal) {
|
|
|
5115
5469
|
}
|
|
5116
5470
|
});
|
|
5117
5471
|
}
|
|
5118
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
5472
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
5119
5473
|
const options = retryOptions ? {
|
|
5120
5474
|
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
5121
5475
|
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
@@ -5126,17 +5480,42 @@ async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
|
5126
5480
|
while (true) {
|
|
5127
5481
|
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
5128
5482
|
try {
|
|
5129
|
-
return await
|
|
5483
|
+
return await synthesizeSsmlOnce(ssml, config);
|
|
5130
5484
|
} catch (error) {
|
|
5131
5485
|
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
5132
5486
|
throw error;
|
|
5133
5487
|
attempt += 1;
|
|
5134
5488
|
const delayMs = retryDelay(options, attempt, error);
|
|
5489
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
5490
|
+
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
5491
|
+
throw new SynthesisTimeoutError(
|
|
5492
|
+
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
5493
|
+
);
|
|
5494
|
+
}
|
|
5135
5495
|
onRetry(attempt, delayMs);
|
|
5136
|
-
await waitForRetry(delayMs, config.signal);
|
|
5496
|
+
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
5137
5497
|
}
|
|
5138
5498
|
}
|
|
5139
5499
|
}
|
|
5500
|
+
async function synthesizeSsml(ssml, config) {
|
|
5501
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
5502
|
+
try {
|
|
5503
|
+
deadline.throwIfExpired();
|
|
5504
|
+
const synthesisConfig = {
|
|
5505
|
+
...config,
|
|
5506
|
+
signal: deadline.signal,
|
|
5507
|
+
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
5508
|
+
};
|
|
5509
|
+
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
5510
|
+
deadline.throwIfExpired();
|
|
5511
|
+
return result;
|
|
5512
|
+
} catch (error) {
|
|
5513
|
+
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
5514
|
+
throw error;
|
|
5515
|
+
} finally {
|
|
5516
|
+
deadline.dispose();
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5140
5519
|
function createAbortScope(parent, timeoutMs) {
|
|
5141
5520
|
const controller = new AbortController();
|
|
5142
5521
|
let didTimeout = false;
|
|
@@ -5157,10 +5536,10 @@ function createAbortScope(parent, timeoutMs) {
|
|
|
5157
5536
|
abort: () => controller.abort()
|
|
5158
5537
|
};
|
|
5159
5538
|
}
|
|
5160
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
5539
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
5161
5540
|
const scope = createAbortScope(config.signal, timeoutMs);
|
|
5162
5541
|
try {
|
|
5163
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
5542
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
5164
5543
|
} catch (error) {
|
|
5165
5544
|
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
5166
5545
|
throw error;
|
|
@@ -5169,18 +5548,42 @@ async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs,
|
|
|
5169
5548
|
}
|
|
5170
5549
|
}
|
|
5171
5550
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
5172
|
-
const results = new Array(chunks.length);
|
|
5173
5551
|
const totalChunks = chunks.length;
|
|
5552
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
5553
|
+
const fingerprints = inputs.map(
|
|
5554
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
5555
|
+
region: config.region,
|
|
5556
|
+
endpoint: config.endpoint,
|
|
5557
|
+
customHeaders: config.customHeaders,
|
|
5558
|
+
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
5559
|
+
})
|
|
5560
|
+
);
|
|
5561
|
+
const results = new Array(totalChunks);
|
|
5174
5562
|
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
5563
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
5564
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
5565
|
+
chunkIndex,
|
|
5566
|
+
status: "pending",
|
|
5567
|
+
canResume: true
|
|
5568
|
+
}));
|
|
5175
5569
|
for (const [index, cached] of cachedChunks) {
|
|
5176
|
-
if (index
|
|
5570
|
+
if (index < 0 || index >= totalChunks) continue;
|
|
5571
|
+
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
5572
|
+
if (isValid) {
|
|
5573
|
+
results[index] = { ...cached };
|
|
5574
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
5575
|
+
} else {
|
|
5576
|
+
invalidCachedIndices.add(index);
|
|
5577
|
+
}
|
|
5177
5578
|
}
|
|
5178
5579
|
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));
|
|
5580
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
5581
|
+
const jobStartedAt = Date.now();
|
|
5582
|
+
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
5583
|
+
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
5180
5584
|
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
5181
5585
|
const report = (event) => config.onProgress?.(event);
|
|
5182
|
-
for (const [index,
|
|
5183
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5586
|
+
for (const [index, input] of inputs.entries()) {
|
|
5184
5587
|
report({
|
|
5185
5588
|
currentChunk: index,
|
|
5186
5589
|
totalChunks,
|
|
@@ -5202,8 +5605,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5202
5605
|
if (index >= chunks.length) return;
|
|
5203
5606
|
if (!shouldSynthesize(index)) continue;
|
|
5204
5607
|
if (firstError && config.cancelOnFailure !== false) return;
|
|
5205
|
-
const
|
|
5206
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5608
|
+
const input = inputs[index];
|
|
5207
5609
|
report({
|
|
5208
5610
|
currentChunk: completed,
|
|
5209
5611
|
totalChunks,
|
|
@@ -5219,7 +5621,7 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5219
5621
|
input.ssml,
|
|
5220
5622
|
{
|
|
5221
5623
|
...config,
|
|
5222
|
-
signal:
|
|
5624
|
+
signal: deadline.signal,
|
|
5223
5625
|
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
5224
5626
|
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
5225
5627
|
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
@@ -5240,9 +5642,11 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5240
5642
|
retryAttempt,
|
|
5241
5643
|
nextRetryDelayMs,
|
|
5242
5644
|
isRetrying: true
|
|
5243
|
-
})
|
|
5645
|
+
}),
|
|
5646
|
+
jobDeadlineAt
|
|
5244
5647
|
);
|
|
5245
5648
|
results[index] = result;
|
|
5649
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
5246
5650
|
completed += 1;
|
|
5247
5651
|
report({
|
|
5248
5652
|
currentChunk: completed,
|
|
@@ -5254,7 +5658,16 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5254
5658
|
durationMs: Date.now() - startedAt
|
|
5255
5659
|
});
|
|
5256
5660
|
} catch (error) {
|
|
5257
|
-
|
|
5661
|
+
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
5662
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
5663
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
5664
|
+
chunkStates[index] = {
|
|
5665
|
+
chunkIndex: index,
|
|
5666
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
5667
|
+
isOriginalFailure: !wasCancelled,
|
|
5668
|
+
canResume: true,
|
|
5669
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
5670
|
+
};
|
|
5258
5671
|
report({
|
|
5259
5672
|
currentChunk: completed,
|
|
5260
5673
|
totalChunks,
|
|
@@ -5265,7 +5678,6 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5265
5678
|
durationMs: Date.now() - startedAt,
|
|
5266
5679
|
error
|
|
5267
5680
|
});
|
|
5268
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
5269
5681
|
if (config.cancelOnFailure !== false) scope.abort();
|
|
5270
5682
|
return;
|
|
5271
5683
|
}
|
|
@@ -5274,26 +5686,47 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
5274
5686
|
try {
|
|
5275
5687
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
5276
5688
|
if (firstError) throw firstError;
|
|
5689
|
+
const missingChunkIndices = Array.from(
|
|
5690
|
+
{ length: totalChunks },
|
|
5691
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
5692
|
+
).filter((index) => index !== void 0);
|
|
5693
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
5277
5694
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
5278
5695
|
return await mergeSynthesisResults(orderedResults, {
|
|
5279
5696
|
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
5280
|
-
signal:
|
|
5697
|
+
signal: deadline.signal,
|
|
5281
5698
|
customMerger: config.customMerger,
|
|
5282
5699
|
outputMimeType: config.outputMimeType,
|
|
5283
|
-
postMergeValidator: config.postMergeValidator
|
|
5700
|
+
postMergeValidator: config.postMergeValidator,
|
|
5701
|
+
deadline
|
|
5284
5702
|
});
|
|
5285
5703
|
} catch (error) {
|
|
5704
|
+
if (firstError && config.cancelOnFailure !== false) {
|
|
5705
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
5706
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
5707
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
const synthesizedChunks = results.flatMap(
|
|
5712
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
5713
|
+
);
|
|
5286
5714
|
const partial = {
|
|
5287
|
-
synthesizedChunks
|
|
5288
|
-
completedChunks:
|
|
5289
|
-
pendingChunkIndices:
|
|
5715
|
+
synthesizedChunks,
|
|
5716
|
+
completedChunks: synthesizedChunks,
|
|
5717
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
5718
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
5719
|
+
),
|
|
5290
5720
|
failedChunkIndices: [...failedIndices],
|
|
5721
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
5722
|
+
chunkStates,
|
|
5291
5723
|
totalChunks
|
|
5292
5724
|
};
|
|
5293
5725
|
if (error && typeof error === "object") error.partialResult = partial;
|
|
5294
5726
|
throw error;
|
|
5295
5727
|
} finally {
|
|
5296
5728
|
scope.dispose();
|
|
5729
|
+
deadline.dispose();
|
|
5297
5730
|
}
|
|
5298
5731
|
}
|
|
5299
5732
|
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
@@ -5371,30 +5804,44 @@ function mergeSynthesisResults(results, options) {
|
|
|
5371
5804
|
const format = resolvedOptions?.format;
|
|
5372
5805
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
5373
5806
|
const buffers = results.map((result) => result.audioData);
|
|
5374
|
-
const inputSpecs = results.map((result) =>
|
|
5807
|
+
const inputSpecs = results.map((result) => {
|
|
5808
|
+
if (result.audioSpec) return result.audioSpec;
|
|
5809
|
+
try {
|
|
5810
|
+
return inspectAudioSpecification(result.audioData, format);
|
|
5811
|
+
} catch (error) {
|
|
5812
|
+
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
5813
|
+
throw error;
|
|
5814
|
+
}
|
|
5815
|
+
});
|
|
5375
5816
|
validateAudioSpecifications(inputSpecs);
|
|
5376
|
-
const
|
|
5817
|
+
const deadline = resolvedOptions.deadline;
|
|
5818
|
+
deadline?.throwIfExpired();
|
|
5819
|
+
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
5377
5820
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
5378
5821
|
if (resolvedOptions.customMerger) {
|
|
5379
|
-
return
|
|
5380
|
-
()
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5822
|
+
return withinDeadline(
|
|
5823
|
+
Promise.resolve().then(
|
|
5824
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
5825
|
+
format,
|
|
5826
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
5827
|
+
inputSpecs,
|
|
5828
|
+
signal
|
|
5829
|
+
})
|
|
5830
|
+
),
|
|
5831
|
+
deadline
|
|
5386
5832
|
).then((merged) => {
|
|
5833
|
+
deadline?.throwIfExpired();
|
|
5387
5834
|
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
5835
|
if (signal.aborted) throw new SynthesisCancelledError();
|
|
5391
|
-
const
|
|
5392
|
-
results,
|
|
5836
|
+
const mergedSpec = validateMergedAudioBuffer(
|
|
5393
5837
|
merged,
|
|
5394
5838
|
format,
|
|
5395
|
-
|
|
5396
|
-
|
|
5839
|
+
buffers,
|
|
5840
|
+
inputSpecs,
|
|
5841
|
+
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
5842
|
+
true
|
|
5397
5843
|
);
|
|
5844
|
+
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
5398
5845
|
return Promise.resolve(
|
|
5399
5846
|
resolvedOptions.postMergeValidator?.(result, {
|
|
5400
5847
|
format,
|
|
@@ -5403,6 +5850,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
5403
5850
|
signal
|
|
5404
5851
|
})
|
|
5405
5852
|
).then((valid) => {
|
|
5853
|
+
deadline?.throwIfExpired();
|
|
5406
5854
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
5407
5855
|
return result;
|
|
5408
5856
|
});
|
|
@@ -5413,6 +5861,7 @@ function mergeSynthesisResults(results, options) {
|
|
|
5413
5861
|
});
|
|
5414
5862
|
}
|
|
5415
5863
|
try {
|
|
5864
|
+
deadline?.throwIfExpired();
|
|
5416
5865
|
const result = createMergedResult(
|
|
5417
5866
|
results,
|
|
5418
5867
|
mergeAudioBuffers(buffers, { format }),
|
|
@@ -5428,12 +5877,14 @@ function mergeSynthesisResults(results, options) {
|
|
|
5428
5877
|
signal
|
|
5429
5878
|
});
|
|
5430
5879
|
if (validation instanceof Promise)
|
|
5431
|
-
return validation.then((valid) => {
|
|
5880
|
+
return withinDeadline(validation, deadline).then((valid) => {
|
|
5881
|
+
deadline?.throwIfExpired();
|
|
5432
5882
|
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
5433
5883
|
return result;
|
|
5434
5884
|
});
|
|
5435
5885
|
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
5436
5886
|
}
|
|
5887
|
+
deadline?.throwIfExpired();
|
|
5437
5888
|
return result;
|
|
5438
5889
|
} catch (error) {
|
|
5439
5890
|
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
@@ -5523,7 +5974,7 @@ function resolveConcurrency2(value, total) {
|
|
|
5523
5974
|
if (value === Infinity) return Math.max(1, total);
|
|
5524
5975
|
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
5525
5976
|
}
|
|
5526
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
5977
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
5527
5978
|
const retry = options ? {
|
|
5528
5979
|
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
5529
5980
|
initialDelayMs: options.initialDelayMs,
|
|
@@ -5540,6 +5991,11 @@ async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
|
5540
5991
|
throw error;
|
|
5541
5992
|
attempt += 1;
|
|
5542
5993
|
const delayMs = retryDelayForError(retry, attempt, error);
|
|
5994
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
5995
|
+
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
5996
|
+
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
5997
|
+
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
5998
|
+
}
|
|
5543
5999
|
onRetry(attempt, delayMs);
|
|
5544
6000
|
if (delayMs > 0)
|
|
5545
6001
|
await new Promise((resolve, reject) => {
|
|
@@ -5573,14 +6029,19 @@ function sharedValidationOptions(options, signal) {
|
|
|
5573
6029
|
};
|
|
5574
6030
|
}
|
|
5575
6031
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
5576
|
-
const
|
|
6032
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
6033
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
5577
6034
|
const diagnostics = await Promise.resolve(validateAzureSsml2(ssml, validationOptions));
|
|
5578
|
-
if (
|
|
5579
|
-
const error = toSynthesisError(
|
|
6035
|
+
if (deadline.signal.aborted) {
|
|
6036
|
+
const error = toSynthesisError(
|
|
6037
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
6038
|
+
);
|
|
6039
|
+
deadline.dispose();
|
|
5580
6040
|
return failure(error);
|
|
5581
6041
|
}
|
|
5582
6042
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
5583
6043
|
if (errors.length > 0) {
|
|
6044
|
+
deadline.dispose();
|
|
5584
6045
|
return failure({
|
|
5585
6046
|
kind: "validation-error",
|
|
5586
6047
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
@@ -5593,23 +6054,30 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
5593
6054
|
success: true,
|
|
5594
6055
|
status: "success",
|
|
5595
6056
|
value: await client.synthesizeSsml(ssml, {
|
|
5596
|
-
signal:
|
|
6057
|
+
signal: deadline.signal,
|
|
5597
6058
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
5598
|
-
timeouts: options.timeouts
|
|
6059
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
5599
6060
|
})
|
|
5600
6061
|
};
|
|
5601
6062
|
} catch (error) {
|
|
6063
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
5602
6064
|
const synthesisError = toSynthesisError(error);
|
|
5603
6065
|
return failure(synthesisError);
|
|
6066
|
+
} finally {
|
|
6067
|
+
deadline.dispose();
|
|
5604
6068
|
}
|
|
5605
6069
|
}
|
|
5606
6070
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
6071
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
5607
6072
|
const validationOptions = sharedValidationOptions(
|
|
5608
6073
|
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
5609
|
-
|
|
6074
|
+
deadline.signal
|
|
5610
6075
|
);
|
|
5611
|
-
if (
|
|
5612
|
-
const error = toSynthesisError(
|
|
6076
|
+
if (deadline.signal.aborted) {
|
|
6077
|
+
const error = toSynthesisError(
|
|
6078
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
6079
|
+
);
|
|
6080
|
+
deadline.dispose();
|
|
5613
6081
|
return failure(error);
|
|
5614
6082
|
}
|
|
5615
6083
|
const pending = (index, status, error) => {
|
|
@@ -5641,14 +6109,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5641
6109
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
5642
6110
|
})
|
|
5643
6111
|
);
|
|
5644
|
-
if (
|
|
6112
|
+
if (deadline.signal.aborted) {
|
|
5645
6113
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
6114
|
+
deadline.dispose();
|
|
5646
6115
|
return failure(error);
|
|
5647
6116
|
}
|
|
5648
6117
|
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
5649
6118
|
if (chunkDiagnostics.length > 0) {
|
|
5650
6119
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
5651
6120
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
6121
|
+
deadline.dispose();
|
|
5652
6122
|
return failure(error);
|
|
5653
6123
|
}
|
|
5654
6124
|
let fallbackJobScope;
|
|
@@ -5661,9 +6131,9 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5661
6131
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
5662
6132
|
onProgress: options.onProgress,
|
|
5663
6133
|
outputFormat: options.outputFormat,
|
|
5664
|
-
signal:
|
|
6134
|
+
signal: deadline.signal,
|
|
5665
6135
|
timeoutMs: options.timeoutMs,
|
|
5666
|
-
timeouts: options.timeouts,
|
|
6136
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
5667
6137
|
sourceNodePath: options.sourceNodePath,
|
|
5668
6138
|
concurrency: options.concurrency,
|
|
5669
6139
|
retryOptions: options.retryOptions,
|
|
@@ -5672,18 +6142,39 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5672
6142
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
5673
6143
|
customMerger: options.customMerger,
|
|
5674
6144
|
outputMimeType: options.outputMimeType,
|
|
5675
|
-
postMergeValidator: options.postMergeValidator
|
|
6145
|
+
postMergeValidator: options.postMergeValidator,
|
|
6146
|
+
resumeValidation: options.resumeValidation,
|
|
6147
|
+
customHeaders: options.customHeaders,
|
|
6148
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
5676
6149
|
});
|
|
5677
6150
|
return { ok: true, success: true, status: "success", value };
|
|
5678
6151
|
}
|
|
6152
|
+
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
6153
|
+
const fingerprints = inputs.map(
|
|
6154
|
+
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
6155
|
+
customHeaders: options.customHeaders,
|
|
6156
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
6157
|
+
})
|
|
6158
|
+
);
|
|
5679
6159
|
const results = new Array(chunks.length);
|
|
6160
|
+
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
6161
|
+
chunkIndex,
|
|
6162
|
+
status: "pending",
|
|
6163
|
+
canResume: true
|
|
6164
|
+
}));
|
|
5680
6165
|
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
6166
|
+
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
5681
6167
|
for (const [index, cached] of cachedChunks) {
|
|
5682
|
-
if (index
|
|
6168
|
+
if (index < 0 || index >= chunks.length) continue;
|
|
6169
|
+
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
6170
|
+
results[index] = cached;
|
|
6171
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
6172
|
+
} else invalidCachedIndices.add(index);
|
|
5683
6173
|
}
|
|
5684
6174
|
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));
|
|
5686
|
-
const
|
|
6175
|
+
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
6176
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
6177
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
5687
6178
|
fallbackJobScope = jobScope;
|
|
5688
6179
|
const failedIndices = /* @__PURE__ */ new Set();
|
|
5689
6180
|
let firstError;
|
|
@@ -5695,7 +6186,10 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5695
6186
|
const index = nextIndex++;
|
|
5696
6187
|
if (index >= chunks.length) return;
|
|
5697
6188
|
if (!shouldSynthesize(index)) continue;
|
|
5698
|
-
if (
|
|
6189
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
6190
|
+
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
6191
|
+
return;
|
|
6192
|
+
}
|
|
5699
6193
|
const chunk = chunks[index];
|
|
5700
6194
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
5701
6195
|
const sourceNodePath = input.sourceNodePath;
|
|
@@ -5704,8 +6198,8 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5704
6198
|
const startedAt = Date.now();
|
|
5705
6199
|
try {
|
|
5706
6200
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
5707
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ??
|
|
5708
|
-
const chunkSignal = chunkScope?.signal ??
|
|
6201
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
6202
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
5709
6203
|
let result;
|
|
5710
6204
|
try {
|
|
5711
6205
|
result = await retryableSynthesis(
|
|
@@ -5728,10 +6222,11 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5728
6222
|
retryAttempt,
|
|
5729
6223
|
nextRetryDelayMs,
|
|
5730
6224
|
isRetrying: true
|
|
5731
|
-
})
|
|
6225
|
+
}),
|
|
6226
|
+
jobDeadlineAt
|
|
5732
6227
|
);
|
|
5733
6228
|
} catch (error) {
|
|
5734
|
-
if (chunkScope?.timedOut())
|
|
6229
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
5735
6230
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
5736
6231
|
throw error;
|
|
5737
6232
|
} finally {
|
|
@@ -5780,6 +6275,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5780
6275
|
}))
|
|
5781
6276
|
} : {}
|
|
5782
6277
|
};
|
|
6278
|
+
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
5783
6279
|
completed += 1;
|
|
5784
6280
|
options.onProgress?.({
|
|
5785
6281
|
currentChunk: completed,
|
|
@@ -5791,7 +6287,16 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5791
6287
|
durationMs: Date.now() - startedAt
|
|
5792
6288
|
});
|
|
5793
6289
|
} catch (error) {
|
|
5794
|
-
|
|
6290
|
+
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
6291
|
+
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
6292
|
+
if (!wasCancelled) failedIndices.add(index);
|
|
6293
|
+
chunkStates[index] = {
|
|
6294
|
+
chunkIndex: index,
|
|
6295
|
+
status: wasCancelled ? "cancelled" : "failed",
|
|
6296
|
+
isOriginalFailure: !wasCancelled,
|
|
6297
|
+
canResume: true,
|
|
6298
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
6299
|
+
};
|
|
5795
6300
|
options.onProgress?.({
|
|
5796
6301
|
currentChunk: completed,
|
|
5797
6302
|
totalChunks: chunks.length,
|
|
@@ -5803,23 +6308,41 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5803
6308
|
error
|
|
5804
6309
|
});
|
|
5805
6310
|
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
5806
|
-
firstError ?? (firstError = error);
|
|
5807
6311
|
return;
|
|
5808
6312
|
}
|
|
5809
6313
|
}
|
|
5810
6314
|
};
|
|
5811
6315
|
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
6316
|
+
if (firstError && options.cancelOnFailure !== false) {
|
|
6317
|
+
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
6318
|
+
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
6319
|
+
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
5812
6323
|
if (failedIndices.size > 0) {
|
|
5813
6324
|
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
6325
|
+
const synthesizedChunks = results.flatMap(
|
|
6326
|
+
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
6327
|
+
);
|
|
5814
6328
|
error.partialResult = {
|
|
5815
|
-
synthesizedChunks
|
|
5816
|
-
completedChunks:
|
|
5817
|
-
pendingChunkIndices:
|
|
6329
|
+
synthesizedChunks,
|
|
6330
|
+
completedChunks: synthesizedChunks,
|
|
6331
|
+
pendingChunkIndices: chunkStates.flatMap(
|
|
6332
|
+
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
6333
|
+
),
|
|
5818
6334
|
failedChunkIndices: [...failedIndices],
|
|
6335
|
+
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
6336
|
+
chunkStates,
|
|
5819
6337
|
totalChunks: chunks.length
|
|
5820
6338
|
};
|
|
5821
6339
|
throw error;
|
|
5822
6340
|
}
|
|
6341
|
+
const missingChunkIndices = Array.from(
|
|
6342
|
+
{ length: chunks.length },
|
|
6343
|
+
(_value, index) => results[index] === void 0 ? index : void 0
|
|
6344
|
+
).filter((index) => index !== void 0);
|
|
6345
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
5823
6346
|
const orderedResults = results.filter((result) => result !== void 0);
|
|
5824
6347
|
return {
|
|
5825
6348
|
ok: true,
|
|
@@ -5827,7 +6350,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5827
6350
|
status: "success",
|
|
5828
6351
|
value: await mergeSynthesisResults(orderedResults, {
|
|
5829
6352
|
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
5830
|
-
signal: jobScope?.signal ??
|
|
6353
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
5831
6354
|
customMerger: options.customMerger,
|
|
5832
6355
|
outputMimeType: options.outputMimeType,
|
|
5833
6356
|
postMergeValidator: options.postMergeValidator
|
|
@@ -5838,6 +6361,7 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
5838
6361
|
return failure(synthesisError, partialResultFrom(error));
|
|
5839
6362
|
} finally {
|
|
5840
6363
|
fallbackJobScope?.dispose();
|
|
6364
|
+
deadline.dispose();
|
|
5841
6365
|
}
|
|
5842
6366
|
}
|
|
5843
6367
|
function withValidationSignal(options, signal) {
|
|
@@ -5858,14 +6382,43 @@ var AzureTtsClient = class {
|
|
|
5858
6382
|
__privateSet(this, _options, options);
|
|
5859
6383
|
}
|
|
5860
6384
|
async synthesize(ssml) {
|
|
5861
|
-
const {
|
|
6385
|
+
const {
|
|
6386
|
+
region,
|
|
6387
|
+
subscriptionKey,
|
|
6388
|
+
outputFormat,
|
|
6389
|
+
signal,
|
|
6390
|
+
timeoutMs,
|
|
6391
|
+
timeouts,
|
|
6392
|
+
customHeaders,
|
|
6393
|
+
fingerprintSchemaVersion
|
|
6394
|
+
} = __privateGet(this, _options);
|
|
5862
6395
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
5863
6396
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
5864
|
-
const config = {
|
|
6397
|
+
const config = {
|
|
6398
|
+
endpoint,
|
|
6399
|
+
region,
|
|
6400
|
+
subscriptionKey,
|
|
6401
|
+
outputFormat,
|
|
6402
|
+
signal,
|
|
6403
|
+
timeoutMs,
|
|
6404
|
+
timeouts,
|
|
6405
|
+
retryOptions: __privateGet(this, _options).retryOptions,
|
|
6406
|
+
customHeaders,
|
|
6407
|
+
fingerprintSchemaVersion
|
|
6408
|
+
};
|
|
5865
6409
|
return synthesizeSpeech(ssml, config);
|
|
5866
6410
|
}
|
|
5867
6411
|
async synthesizeSsml(ssml, options = {}) {
|
|
5868
|
-
const {
|
|
6412
|
+
const {
|
|
6413
|
+
region,
|
|
6414
|
+
subscriptionKey,
|
|
6415
|
+
outputFormat,
|
|
6416
|
+
signal,
|
|
6417
|
+
timeoutMs,
|
|
6418
|
+
timeouts,
|
|
6419
|
+
customHeaders,
|
|
6420
|
+
fingerprintSchemaVersion
|
|
6421
|
+
} = __privateGet(this, _options);
|
|
5869
6422
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
5870
6423
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
5871
6424
|
return synthesizeSsml(ssml, {
|
|
@@ -5878,11 +6431,28 @@ var AzureTtsClient = class {
|
|
|
5878
6431
|
timeouts: options.timeouts ?? timeouts,
|
|
5879
6432
|
sourceNodePath: options.sourceNodePath,
|
|
5880
6433
|
sourceTextSegments: options.sourceTextSegments,
|
|
5881
|
-
sourceMarkers: options.sourceMarkers
|
|
6434
|
+
sourceMarkers: options.sourceMarkers,
|
|
6435
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
6436
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
6437
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
6438
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
6439
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
6440
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
6441
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
6442
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
5882
6443
|
});
|
|
5883
6444
|
}
|
|
5884
6445
|
async synthesizeChunks(chunks, options = {}) {
|
|
5885
|
-
const {
|
|
6446
|
+
const {
|
|
6447
|
+
region,
|
|
6448
|
+
subscriptionKey,
|
|
6449
|
+
outputFormat,
|
|
6450
|
+
signal,
|
|
6451
|
+
timeoutMs,
|
|
6452
|
+
timeouts,
|
|
6453
|
+
customHeaders,
|
|
6454
|
+
fingerprintSchemaVersion
|
|
6455
|
+
} = __privateGet(this, _options);
|
|
5886
6456
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
5887
6457
|
return synthesizeSsmlChunks(chunks, {
|
|
5888
6458
|
endpoint,
|
|
@@ -5896,12 +6466,15 @@ var AzureTtsClient = class {
|
|
|
5896
6466
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
5897
6467
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
5898
6468
|
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
5899
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
6469
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
5900
6470
|
resumeChunks: options.resumeChunks,
|
|
5901
6471
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
5902
|
-
customMerger: options.customMerger,
|
|
5903
|
-
outputMimeType: options.outputMimeType,
|
|
5904
|
-
postMergeValidator: options.postMergeValidator
|
|
6472
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
6473
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
6474
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
6475
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
6476
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
6477
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
5905
6478
|
});
|
|
5906
6479
|
}
|
|
5907
6480
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -5916,7 +6489,14 @@ var AzureTtsClient = class {
|
|
|
5916
6489
|
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
5917
6490
|
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
5918
6491
|
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
5919
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
6492
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
6493
|
+
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
6494
|
+
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
6495
|
+
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
6496
|
+
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
6497
|
+
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
6498
|
+
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
6499
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
5920
6500
|
});
|
|
5921
6501
|
}
|
|
5922
6502
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -6022,6 +6602,8 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
6022
6602
|
BatchChunkValidationError,
|
|
6023
6603
|
ChunkValidationError,
|
|
6024
6604
|
DEFAULT_OUTPUT_FORMAT,
|
|
6605
|
+
DeadlineController,
|
|
6606
|
+
IncompleteChunkSetError,
|
|
6025
6607
|
MergeError,
|
|
6026
6608
|
SynthesisCancelledError,
|
|
6027
6609
|
SynthesisTimeoutError,
|
|
@@ -6030,6 +6612,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
6030
6612
|
buildPartialSsml,
|
|
6031
6613
|
buildSsml,
|
|
6032
6614
|
canMergeAudioFormat,
|
|
6615
|
+
computeChunkFingerprint,
|
|
6033
6616
|
createAzureUrlValidatorRunner,
|
|
6034
6617
|
extractSsmlText,
|
|
6035
6618
|
extractSsmlTranslatableText,
|
|
@@ -6048,6 +6631,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
6048
6631
|
parseSsml,
|
|
6049
6632
|
resolveMergeAudioFormat,
|
|
6050
6633
|
resolveMimeType,
|
|
6634
|
+
serializeChunkError,
|
|
6051
6635
|
splitSsmlDocument,
|
|
6052
6636
|
synthesizeSpeech,
|
|
6053
6637
|
synthesizeSsml,
|