@ssml-builder-js/azure-tts-client 2.19.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/package.json +8 -8
- package/test/synthesis.test.ts +20 -0
- package/test/v219-pipeline.test.ts +136 -2
- package/dist/index.d.mts +0 -538
- package/dist/index.d.ts +0 -538
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2200
- package/dist/index.mjs.map +0 -1
package/dist/index.mjs
DELETED
|
@@ -1,2200 +0,0 @@
|
|
|
1
|
-
var __typeError = (msg) => {
|
|
2
|
-
throw TypeError(msg);
|
|
3
|
-
};
|
|
4
|
-
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
-
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
-
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
|
|
9
|
-
// src/errors.ts
|
|
10
|
-
var AzureTtsError = class extends Error {
|
|
11
|
-
constructor(status, statusText, responseBody, requestId, responseHeaders) {
|
|
12
|
-
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
13
|
-
this.kind = "azure-api-error";
|
|
14
|
-
this.name = "AzureTtsError";
|
|
15
|
-
this.status = status;
|
|
16
|
-
this.statusText = statusText;
|
|
17
|
-
this.responseBody = responseBody;
|
|
18
|
-
this.requestId = requestId;
|
|
19
|
-
const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
|
|
20
|
-
const seconds = value ? Number(value.trim()) : NaN;
|
|
21
|
-
const date = value ? Date.parse(value) : NaN;
|
|
22
|
-
if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
|
|
23
|
-
else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
function getRetryAfterDelayMs(error) {
|
|
27
|
-
if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
|
|
28
|
-
if (!error || typeof error !== "object") return void 0;
|
|
29
|
-
const candidate = error;
|
|
30
|
-
if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
|
|
31
|
-
const headers = candidate.headers ?? candidate.response?.headers;
|
|
32
|
-
if (headers instanceof Headers) {
|
|
33
|
-
const value = headers.get("retry-after");
|
|
34
|
-
if (!value) return void 0;
|
|
35
|
-
const seconds = Number(value.trim());
|
|
36
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
37
|
-
const date = Date.parse(value);
|
|
38
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
39
|
-
}
|
|
40
|
-
if (headers && typeof headers === "object") {
|
|
41
|
-
const value = headers["retry-after"] ?? headers["Retry-After"];
|
|
42
|
-
if (typeof value !== "string") return void 0;
|
|
43
|
-
const seconds = Number(value.trim());
|
|
44
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
45
|
-
const date = Date.parse(value);
|
|
46
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
47
|
-
}
|
|
48
|
-
return void 0;
|
|
49
|
-
}
|
|
50
|
-
var AzureTtsSdkError = class extends AzureTtsError {
|
|
51
|
-
constructor(errorDetails) {
|
|
52
|
-
super(0, "Speech SDK", errorDetails, null);
|
|
53
|
-
this.name = "AzureTtsSdkError";
|
|
54
|
-
this.message = `Azure TTS synthesis failed: ${errorDetails}`;
|
|
55
|
-
this.errorDetails = errorDetails;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var SynthesisCancelledError = class extends Error {
|
|
59
|
-
constructor(message = "Speech synthesis was cancelled.") {
|
|
60
|
-
super(message);
|
|
61
|
-
this.kind = "cancelled";
|
|
62
|
-
this.name = "SynthesisCancelledError";
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
var SynthesisTimeoutError = class extends Error {
|
|
66
|
-
constructor(message) {
|
|
67
|
-
super(message);
|
|
68
|
-
this.kind = "timeout";
|
|
69
|
-
this.name = "SynthesisTimeoutError";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
var IncompleteChunkSetError = class extends Error {
|
|
73
|
-
constructor(totalChunks, missingChunkIndices) {
|
|
74
|
-
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
75
|
-
this.kind = "incomplete-chunk-set";
|
|
76
|
-
this.name = "IncompleteChunkSetError";
|
|
77
|
-
this.totalChunks = totalChunks;
|
|
78
|
-
this.missingChunkIndices = [...missingChunkIndices];
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
var MergeError = class extends Error {
|
|
82
|
-
constructor(message, cause) {
|
|
83
|
-
super(message);
|
|
84
|
-
this.kind = "merge-error";
|
|
85
|
-
this.name = "MergeError";
|
|
86
|
-
this.cause = cause;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var AudioFormatMismatchError = class extends Error {
|
|
90
|
-
constructor(message, inputSpecs = []) {
|
|
91
|
-
super(message);
|
|
92
|
-
this.kind = "audio-format-mismatch";
|
|
93
|
-
this.name = "AudioFormatMismatchError";
|
|
94
|
-
this.inputSpecs = inputSpecs;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
var UnsupportedMergeFormatError = class extends Error {
|
|
98
|
-
constructor(format) {
|
|
99
|
-
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
100
|
-
this.kind = "unsupported-format-error";
|
|
101
|
-
this.name = "UnsupportedMergeFormatError";
|
|
102
|
-
this.format = format;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
function serializeChunkError(error, phase, isOriginalFailure) {
|
|
106
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
-
const status = error instanceof AzureTtsError ? error.status : void 0;
|
|
108
|
-
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
109
|
-
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";
|
|
110
|
-
const details = {};
|
|
111
|
-
if (error instanceof AzureTtsError) {
|
|
112
|
-
details.statusText = error.statusText;
|
|
113
|
-
if (error.requestId) details.requestId = error.requestId;
|
|
114
|
-
}
|
|
115
|
-
if (error instanceof IncompleteChunkSetError) {
|
|
116
|
-
details.totalChunks = error.totalChunks;
|
|
117
|
-
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
code,
|
|
121
|
-
phase,
|
|
122
|
-
message,
|
|
123
|
-
isOriginalFailure,
|
|
124
|
-
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)),
|
|
125
|
-
...status !== void 0 && status > 0 ? { httpStatus: status } : {},
|
|
126
|
-
...Object.keys(details).length > 0 ? { details } : {}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function toSynthesisError(error) {
|
|
130
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError || error instanceof IncompleteChunkSetError)
|
|
131
|
-
return error;
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
-
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
134
|
-
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
135
|
-
return createSpeechSdkError(error);
|
|
136
|
-
}
|
|
137
|
-
function createSpeechSdkError(error) {
|
|
138
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
-
return new AzureTtsSdkError(message);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/deadline.ts
|
|
143
|
-
var _controller, _parent, _onParentAbort, _timer, _timedOut;
|
|
144
|
-
var DeadlineController = class {
|
|
145
|
-
constructor(totalJobMs, parent) {
|
|
146
|
-
__privateAdd(this, _controller, new AbortController());
|
|
147
|
-
__privateAdd(this, _parent);
|
|
148
|
-
__privateAdd(this, _onParentAbort);
|
|
149
|
-
__privateAdd(this, _timer);
|
|
150
|
-
__privateAdd(this, _timedOut, false);
|
|
151
|
-
__privateSet(this, _parent, parent);
|
|
152
|
-
__privateSet(this, _onParentAbort, () => __privateGet(this, _controller).abort());
|
|
153
|
-
this.deadlineAtMs = totalJobMs !== void 0 && totalJobMs > 0 ? Date.now() + totalJobMs : void 0;
|
|
154
|
-
this.signal = this.deadlineAtMs === void 0 && parent ? parent : __privateGet(this, _controller).signal;
|
|
155
|
-
if (parent?.aborted) __privateGet(this, _controller).abort();
|
|
156
|
-
parent?.addEventListener("abort", __privateGet(this, _onParentAbort), { once: true });
|
|
157
|
-
if (this.deadlineAtMs !== void 0) {
|
|
158
|
-
__privateSet(this, _timer, setTimeout(
|
|
159
|
-
() => {
|
|
160
|
-
__privateSet(this, _timedOut, true);
|
|
161
|
-
__privateGet(this, _controller).abort();
|
|
162
|
-
},
|
|
163
|
-
Math.max(0, this.deadlineAtMs - Date.now())
|
|
164
|
-
));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
get timedOut() {
|
|
168
|
-
return __privateGet(this, _timedOut) || this.deadlineAtMs !== void 0 && this.remainingMs <= 0;
|
|
169
|
-
}
|
|
170
|
-
get remainingMs() {
|
|
171
|
-
return this.deadlineAtMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
172
|
-
}
|
|
173
|
-
throwIfExpired() {
|
|
174
|
-
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
175
|
-
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
176
|
-
}
|
|
177
|
-
abort() {
|
|
178
|
-
__privateGet(this, _controller).abort();
|
|
179
|
-
}
|
|
180
|
-
dispose() {
|
|
181
|
-
if (__privateGet(this, _timer)) clearTimeout(__privateGet(this, _timer));
|
|
182
|
-
__privateGet(this, _parent)?.removeEventListener("abort", __privateGet(this, _onParentAbort));
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
_controller = new WeakMap();
|
|
186
|
-
_parent = new WeakMap();
|
|
187
|
-
_onParentAbort = new WeakMap();
|
|
188
|
-
_timer = new WeakMap();
|
|
189
|
-
_timedOut = new WeakMap();
|
|
190
|
-
|
|
191
|
-
// src/synthesis.ts
|
|
192
|
-
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
193
|
-
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
194
|
-
|
|
195
|
-
// src/outputFormats.ts
|
|
196
|
-
import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
|
|
197
|
-
var DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
|
|
198
|
-
var OUTPUT_FORMATS = {
|
|
199
|
-
"raw-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw,
|
|
200
|
-
"riff-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren,
|
|
201
|
-
"audio-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren,
|
|
202
|
-
"audio-16khz-32kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3,
|
|
203
|
-
"audio-16khz-128kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3,
|
|
204
|
-
"audio-16khz-64kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz64KBitRateMonoMp3,
|
|
205
|
-
"audio-24khz-48kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3,
|
|
206
|
-
"audio-24khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3,
|
|
207
|
-
"audio-24khz-160kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3,
|
|
208
|
-
"raw-16khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoTrueSilk,
|
|
209
|
-
"riff-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm,
|
|
210
|
-
"riff-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz16BitMonoPcm,
|
|
211
|
-
"riff-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm,
|
|
212
|
-
"riff-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw,
|
|
213
|
-
"raw-16khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm,
|
|
214
|
-
"raw-24khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm,
|
|
215
|
-
"raw-8khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm,
|
|
216
|
-
"ogg-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg16Khz16BitMonoOpus,
|
|
217
|
-
"ogg-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg24Khz16BitMonoOpus,
|
|
218
|
-
"raw-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw48Khz16BitMonoPcm,
|
|
219
|
-
"riff-48khz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm,
|
|
220
|
-
"audio-48khz-96kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz96KBitRateMonoMp3,
|
|
221
|
-
"audio-48khz-192kbitrate-mono-mp3": SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3,
|
|
222
|
-
"ogg-48khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Ogg48Khz16BitMonoOpus,
|
|
223
|
-
"webm-16khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm16Khz16BitMonoOpus,
|
|
224
|
-
"webm-24khz-16bit-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16BitMonoOpus,
|
|
225
|
-
"webm-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Webm24Khz16Bit24KbpsMonoOpus,
|
|
226
|
-
"raw-24khz-16bit-mono-truesilk": SpeechSDK.SpeechSynthesisOutputFormat.Raw24Khz16BitMonoTrueSilk,
|
|
227
|
-
"raw-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoALaw,
|
|
228
|
-
"riff-8khz-8bit-mono-alaw": SpeechSDK.SpeechSynthesisOutputFormat.Riff8Khz8BitMonoALaw,
|
|
229
|
-
"audio-16khz-16bit-32kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16Bit32KbpsMonoOpus,
|
|
230
|
-
"audio-24khz-16bit-48kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit48KbpsMonoOpus,
|
|
231
|
-
"audio-24khz-16bit-24kbps-mono-opus": SpeechSDK.SpeechSynthesisOutputFormat.Audio24Khz16Bit24KbpsMonoOpus,
|
|
232
|
-
"raw-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm,
|
|
233
|
-
"riff-22050hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff22050Hz16BitMonoPcm,
|
|
234
|
-
"raw-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Raw44100Hz16BitMonoPcm,
|
|
235
|
-
"riff-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm,
|
|
236
|
-
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
237
|
-
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
238
|
-
};
|
|
239
|
-
function resolveMimeType(outputFormat) {
|
|
240
|
-
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
241
|
-
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
242
|
-
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
243
|
-
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
244
|
-
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
245
|
-
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
246
|
-
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
247
|
-
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
248
|
-
return "application/octet-stream";
|
|
249
|
-
}
|
|
250
|
-
function resolveOutputFormat(outputFormat) {
|
|
251
|
-
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
252
|
-
if (resolvedFormat === void 0) {
|
|
253
|
-
throw new Error(`Unsupported Azure Speech output format: ${outputFormat}`);
|
|
254
|
-
}
|
|
255
|
-
return resolvedFormat;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// src/speechConfig.ts
|
|
259
|
-
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
260
|
-
function resolveEndpoint(config) {
|
|
261
|
-
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
262
|
-
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
263
|
-
}
|
|
264
|
-
function createSpeechConfig(config) {
|
|
265
|
-
const { outputFormat = DEFAULT_OUTPUT_FORMAT, subscriptionKey } = config;
|
|
266
|
-
const endpoint = new URL(resolveEndpoint(config));
|
|
267
|
-
const speechConfig = SpeechConfig.fromEndpoint(endpoint, subscriptionKey);
|
|
268
|
-
speechConfig.speechSynthesisOutputFormat = resolveOutputFormat(outputFormat);
|
|
269
|
-
return speechConfig;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/synthesis.ts
|
|
273
|
-
function computeChunkFingerprint(ssml, outputFormat = DEFAULT_OUTPUT_FORMAT, options = {}) {
|
|
274
|
-
const readAttribute = (name) => {
|
|
275
|
-
const pattern = new RegExp(`(?:${name})\\s*=\\s*[\\"']([^\\"']*)`, "gi");
|
|
276
|
-
return [...ssml.matchAll(pattern)].map((match) => match[1] ?? "").join("|");
|
|
277
|
-
};
|
|
278
|
-
const headers = Object.fromEntries(
|
|
279
|
-
Object.entries(options.customHeaders ?? {}).sort(([first], [second]) => first.localeCompare(second))
|
|
280
|
-
);
|
|
281
|
-
const payload = JSON.stringify({
|
|
282
|
-
ssml,
|
|
283
|
-
outputFormat,
|
|
284
|
-
region: options.region ?? "",
|
|
285
|
-
endpoint: options.endpoint ?? "",
|
|
286
|
-
voice: options.voice ?? readAttribute("(?:name|voice)"),
|
|
287
|
-
lang: options.lang ?? readAttribute("(?:xml:lang|lang)"),
|
|
288
|
-
customHeaders: headers,
|
|
289
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? options.schemaVersion ?? "2"
|
|
290
|
-
});
|
|
291
|
-
let hash = 0xcbf29ce484222325n;
|
|
292
|
-
const mask = 0xffffffffffffffffn;
|
|
293
|
-
for (let index = 0; index < payload.length; index += 1) {
|
|
294
|
-
hash ^= BigInt(payload.charCodeAt(index));
|
|
295
|
-
hash = hash * 0x100000001b3n & mask;
|
|
296
|
-
}
|
|
297
|
-
return `fnv1a64-${hash.toString(16).padStart(16, "0")}`;
|
|
298
|
-
}
|
|
299
|
-
function ascii(bytes, offset, value) {
|
|
300
|
-
return [...value].every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
301
|
-
}
|
|
302
|
-
function readUint32(bytes, offset) {
|
|
303
|
-
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, true);
|
|
304
|
-
}
|
|
305
|
-
function parseWav(buffer) {
|
|
306
|
-
const bytes = new Uint8Array(buffer);
|
|
307
|
-
if (bytes.byteLength < 12 || !ascii(bytes, 0, "RIFF") || !ascii(bytes, 8, "WAVE")) {
|
|
308
|
-
throw new Error("Invalid WAV/RIFF audio buffer.");
|
|
309
|
-
}
|
|
310
|
-
const chunks = [];
|
|
311
|
-
const dataParts = [];
|
|
312
|
-
let format;
|
|
313
|
-
let offset = 12;
|
|
314
|
-
while (offset < bytes.byteLength) {
|
|
315
|
-
if (offset + 8 > bytes.byteLength) throw new Error("Invalid WAV chunk header.");
|
|
316
|
-
const id = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
|
317
|
-
const size = readUint32(bytes, offset + 4);
|
|
318
|
-
const dataStart = offset + 8;
|
|
319
|
-
const dataEnd = dataStart + size;
|
|
320
|
-
if (dataEnd > bytes.byteLength) throw new Error(`WAV chunk "${id}" exceeds the audio buffer.`);
|
|
321
|
-
const data2 = bytes.slice(dataStart, dataEnd);
|
|
322
|
-
chunks.push({ id, data: data2 });
|
|
323
|
-
if (id === "fmt ") format ?? (format = data2);
|
|
324
|
-
if (id === "data") dataParts.push(data2);
|
|
325
|
-
offset = dataEnd + (size & 1);
|
|
326
|
-
if (offset > bytes.byteLength) throw new Error("Invalid WAV chunk padding.");
|
|
327
|
-
}
|
|
328
|
-
if (!format || dataParts.length === 0) throw new Error("WAV audio must contain fmt and data chunks.");
|
|
329
|
-
const dataLength = dataParts.reduce((total, part) => total + part.byteLength, 0);
|
|
330
|
-
const data = new Uint8Array(dataLength);
|
|
331
|
-
let dataOffset = 0;
|
|
332
|
-
for (const part of dataParts) {
|
|
333
|
-
data.set(part, dataOffset);
|
|
334
|
-
dataOffset += part.byteLength;
|
|
335
|
-
}
|
|
336
|
-
return { chunks, data, format };
|
|
337
|
-
}
|
|
338
|
-
function formatSampleRate(format) {
|
|
339
|
-
const match = /(?:^|-)(\d+)(khz|hz)(?:-|$)/i.exec(format);
|
|
340
|
-
if (!match?.[1] || !match[2]) return 0;
|
|
341
|
-
const value = Number(match[1]);
|
|
342
|
-
return match[2].toLowerCase() === "khz" ? value * 1e3 : value;
|
|
343
|
-
}
|
|
344
|
-
function formatChannels(format, fallback) {
|
|
345
|
-
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
346
|
-
if (/mono|1ch/i.test(format)) return 1;
|
|
347
|
-
return fallback;
|
|
348
|
-
}
|
|
349
|
-
function formatAudioSpecification(format) {
|
|
350
|
-
const sampleRate = formatSampleRate(format);
|
|
351
|
-
const channels = formatChannels(format, 0);
|
|
352
|
-
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
353
|
-
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
354
|
-
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";
|
|
355
|
-
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
356
|
-
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;
|
|
357
|
-
return {
|
|
358
|
-
format,
|
|
359
|
-
mimeType: resolveMimeType(format),
|
|
360
|
-
codec,
|
|
361
|
-
sampleRate,
|
|
362
|
-
channels,
|
|
363
|
-
...bitrate ? { bitrate } : {},
|
|
364
|
-
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
365
|
-
...container ? { container } : {},
|
|
366
|
-
isVbr: /vbr/i.test(format),
|
|
367
|
-
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
function parseMp3Specification(buffer, format) {
|
|
371
|
-
const bytes = stripMp3Tags(buffer);
|
|
372
|
-
const bitrates = [
|
|
373
|
-
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
374
|
-
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
375
|
-
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
376
|
-
];
|
|
377
|
-
const sampleRates = [
|
|
378
|
-
[44100, 48e3, 32e3],
|
|
379
|
-
[22050, 24e3, 16e3],
|
|
380
|
-
[11025, 12e3, 8e3]
|
|
381
|
-
];
|
|
382
|
-
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
383
|
-
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
384
|
-
const header = bytes[index + 1] ?? 0;
|
|
385
|
-
const versionBits = header >> 3 & 3;
|
|
386
|
-
const layer = header >> 1 & 3;
|
|
387
|
-
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
388
|
-
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
389
|
-
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
390
|
-
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
391
|
-
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
392
|
-
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
393
|
-
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
394
|
-
if (!sampleRate || !bitrateKbps) continue;
|
|
395
|
-
return {
|
|
396
|
-
format,
|
|
397
|
-
mimeType: "audio/mpeg",
|
|
398
|
-
codec: "mp3",
|
|
399
|
-
sampleRate,
|
|
400
|
-
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
401
|
-
bitrate: bitrateKbps * 1e3,
|
|
402
|
-
container: "mp3-raw",
|
|
403
|
-
isVbr: false,
|
|
404
|
-
isCompressed: true
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
return void 0;
|
|
408
|
-
}
|
|
409
|
-
function readEbmlVint(bytes, offset, preserveMarker) {
|
|
410
|
-
const first = bytes[offset];
|
|
411
|
-
if (first === void 0) throw new Error("Invalid EBML variable-length integer.");
|
|
412
|
-
let mask = 128;
|
|
413
|
-
let length = 1;
|
|
414
|
-
while (length <= 8 && (first & mask) === 0) {
|
|
415
|
-
mask >>= 1;
|
|
416
|
-
length += 1;
|
|
417
|
-
}
|
|
418
|
-
if (length > 8 || offset + length > bytes.byteLength) throw new Error("Truncated EBML variable-length integer.");
|
|
419
|
-
let value = preserveMarker ? first : first & mask - 1;
|
|
420
|
-
for (let index = 1; index < length; index += 1) value = value * 256 + (bytes[offset + index] ?? 0);
|
|
421
|
-
if (!preserveMarker && value === 2 ** (7 * length) - 1)
|
|
422
|
-
throw new Error("EBML unknown-size elements are not supported.");
|
|
423
|
-
return { value, length };
|
|
424
|
-
}
|
|
425
|
-
function readEbmlElement(bytes, offset) {
|
|
426
|
-
const id = readEbmlVint(bytes, offset, true);
|
|
427
|
-
const size = readEbmlVint(bytes, offset + id.length, false);
|
|
428
|
-
const dataStart = offset + id.length + size.length;
|
|
429
|
-
const dataEnd = dataStart + size.value;
|
|
430
|
-
if (dataEnd > bytes.byteLength) throw new Error("EBML element exceeds the audio buffer.");
|
|
431
|
-
return { id: id.value, dataStart, dataEnd };
|
|
432
|
-
}
|
|
433
|
-
function ebmlText(bytes, element) {
|
|
434
|
-
return new TextDecoder().decode(bytes.slice(element.dataStart, element.dataEnd));
|
|
435
|
-
}
|
|
436
|
-
function findEbmlElement(bytes, start, end, id) {
|
|
437
|
-
let offset = start;
|
|
438
|
-
while (offset < end) {
|
|
439
|
-
const element = readEbmlElement(bytes, offset);
|
|
440
|
-
if (element.id === id) return element;
|
|
441
|
-
offset = element.dataEnd;
|
|
442
|
-
}
|
|
443
|
-
if (offset !== end) throw new Error("Invalid EBML element boundary.");
|
|
444
|
-
return void 0;
|
|
445
|
-
}
|
|
446
|
-
function parseOggSpecification(buffer, format) {
|
|
447
|
-
const bytes = new Uint8Array(buffer);
|
|
448
|
-
let offset = 0;
|
|
449
|
-
let firstPayload;
|
|
450
|
-
let pages = 0;
|
|
451
|
-
while (offset < bytes.byteLength) {
|
|
452
|
-
if (offset + 27 > bytes.byteLength || !ascii(bytes, offset, "OggS")) throw new Error("Invalid Ogg page header.");
|
|
453
|
-
if (bytes[offset + 4] !== 0) throw new Error("Unsupported Ogg bitstream version.");
|
|
454
|
-
const segmentCount = bytes[offset + 26] ?? 0;
|
|
455
|
-
const lacingStart = offset + 27;
|
|
456
|
-
const payloadStart = lacingStart + segmentCount;
|
|
457
|
-
if (payloadStart > bytes.byteLength) throw new Error("Truncated Ogg segment table.");
|
|
458
|
-
const payloadLength = bytes.slice(lacingStart, payloadStart).reduce((total, value) => total + value, 0);
|
|
459
|
-
const pageEnd = payloadStart + payloadLength;
|
|
460
|
-
if (pageEnd > bytes.byteLength) throw new Error("Ogg page payload exceeds the audio buffer.");
|
|
461
|
-
if (pages === 0) firstPayload = bytes.slice(payloadStart, pageEnd);
|
|
462
|
-
offset = pageEnd;
|
|
463
|
-
pages += 1;
|
|
464
|
-
}
|
|
465
|
-
if (pages === 0 || !firstPayload || !ascii(firstPayload, 0, "OpusHead") || firstPayload.byteLength < 19)
|
|
466
|
-
throw new Error("Ogg audio must contain a valid OpusHead packet.");
|
|
467
|
-
const version = firstPayload[8];
|
|
468
|
-
const channels = firstPayload[9] ?? 0;
|
|
469
|
-
const sampleRate = new DataView(firstPayload.buffer, firstPayload.byteOffset, firstPayload.byteLength).getUint32(
|
|
470
|
-
12,
|
|
471
|
-
true
|
|
472
|
-
);
|
|
473
|
-
if (version !== 1 || channels <= 0 || sampleRate <= 0) throw new Error("Invalid Ogg OpusHead stream parameters.");
|
|
474
|
-
return {
|
|
475
|
-
format,
|
|
476
|
-
mimeType: "audio/ogg",
|
|
477
|
-
codec: "opus",
|
|
478
|
-
sampleRate,
|
|
479
|
-
channels,
|
|
480
|
-
container: "ogg",
|
|
481
|
-
isVbr: true,
|
|
482
|
-
isCompressed: true
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
function parseWebmSpecification(buffer, format) {
|
|
486
|
-
const bytes = new Uint8Array(buffer);
|
|
487
|
-
const ebml = readEbmlElement(bytes, 0);
|
|
488
|
-
if (ebml.id !== 440786851) throw new Error("WebM audio must begin with an EBML header.");
|
|
489
|
-
const docType = findEbmlElement(bytes, ebml.dataStart, ebml.dataEnd, 17026);
|
|
490
|
-
if (!docType || ebmlText(bytes, docType).toLowerCase() !== "webm") throw new Error("EBML DocType must be webm.");
|
|
491
|
-
const segment = readEbmlElement(bytes, ebml.dataEnd);
|
|
492
|
-
if (segment.id !== 408125543) throw new Error("WebM audio must contain a Segment element.");
|
|
493
|
-
const tracks = findEbmlElement(bytes, segment.dataStart, segment.dataEnd, 374648427);
|
|
494
|
-
if (!tracks) throw new Error("WebM audio must contain a Tracks element.");
|
|
495
|
-
let offset = tracks.dataStart;
|
|
496
|
-
let opusTrack;
|
|
497
|
-
while (offset < tracks.dataEnd) {
|
|
498
|
-
const track = readEbmlElement(bytes, offset);
|
|
499
|
-
if (track.id === 174) {
|
|
500
|
-
const codec = findEbmlElement(bytes, track.dataStart, track.dataEnd, 134);
|
|
501
|
-
const trackType = findEbmlElement(bytes, track.dataStart, track.dataEnd, 131);
|
|
502
|
-
if (codec && ebmlText(bytes, codec) === "A_OPUS" && trackType && bytes[trackType.dataStart] === 2) {
|
|
503
|
-
opusTrack = track;
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
offset = track.dataEnd;
|
|
508
|
-
}
|
|
509
|
-
if (!opusTrack) throw new Error("WebM tracks do not define an Opus audio track.");
|
|
510
|
-
const audio = findEbmlElement(bytes, opusTrack.dataStart, opusTrack.dataEnd, 225);
|
|
511
|
-
const sampling = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 181) : void 0;
|
|
512
|
-
const channels = audio ? findEbmlElement(bytes, audio.dataStart, audio.dataEnd, 159) : void 0;
|
|
513
|
-
const sampleRate = sampling ? new DataView(
|
|
514
|
-
bytes.buffer,
|
|
515
|
-
bytes.byteOffset + sampling.dataStart,
|
|
516
|
-
sampling.dataEnd - sampling.dataStart
|
|
517
|
-
).getFloat64(0, false) : 0;
|
|
518
|
-
const channelCount = channels ? bytes[channels.dataEnd - 1] ?? 0 : 0;
|
|
519
|
-
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || channelCount <= 0)
|
|
520
|
-
throw new Error("WebM Opus audio track has invalid sampling or channel parameters.");
|
|
521
|
-
return {
|
|
522
|
-
format,
|
|
523
|
-
mimeType: "audio/webm",
|
|
524
|
-
codec: "opus",
|
|
525
|
-
sampleRate: Math.round(sampleRate),
|
|
526
|
-
channels: channelCount,
|
|
527
|
-
container: "webm",
|
|
528
|
-
isVbr: true,
|
|
529
|
-
isCompressed: true
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
function inspectAudioSpecification(buffer, format) {
|
|
533
|
-
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
534
|
-
const parsed = parseWav(buffer);
|
|
535
|
-
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
536
|
-
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
537
|
-
const sampleRate = view.getUint32(4, true);
|
|
538
|
-
const channels = view.getUint16(2, true);
|
|
539
|
-
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
540
|
-
const formatCode = view.getUint16(0, true);
|
|
541
|
-
const namedCodec = formatAudioSpecification(format).codec;
|
|
542
|
-
const codec = formatCode === 1 ? "pcm" : formatCode === 6 ? "alaw" : formatCode === 7 ? "mulaw" : namedCodec === "siren" ? "siren" : "unknown";
|
|
543
|
-
return {
|
|
544
|
-
format,
|
|
545
|
-
mimeType: "audio/wav",
|
|
546
|
-
codec,
|
|
547
|
-
sampleRate,
|
|
548
|
-
channels,
|
|
549
|
-
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
550
|
-
bitDepth: bitsPerSample,
|
|
551
|
-
container: "riff-wave",
|
|
552
|
-
isVbr: false,
|
|
553
|
-
isCompressed: codec !== "pcm" && codec !== "unknown"
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
if (isMp3Format(format)) {
|
|
557
|
-
const specification2 = parseMp3Specification(buffer, format);
|
|
558
|
-
return specification2 ?? formatAudioSpecification(format);
|
|
559
|
-
}
|
|
560
|
-
if (isOggFormat(format) || ascii(new Uint8Array(buffer), 0, "OggS")) {
|
|
561
|
-
const specification2 = parseOggSpecification(buffer, format);
|
|
562
|
-
validateContainerFormat(specification2, format);
|
|
563
|
-
return specification2;
|
|
564
|
-
}
|
|
565
|
-
if (isWebmFormat(format) || new Uint8Array(buffer)[0] === 26) {
|
|
566
|
-
const specification2 = parseWebmSpecification(buffer, format);
|
|
567
|
-
validateContainerFormat(specification2, format);
|
|
568
|
-
return specification2;
|
|
569
|
-
}
|
|
570
|
-
const specification = formatAudioSpecification(format);
|
|
571
|
-
if (specification.container === "raw") validateRawAudioBuffer(buffer, specification);
|
|
572
|
-
return specification;
|
|
573
|
-
}
|
|
574
|
-
function validateRawAudioBuffer(buffer, specification) {
|
|
575
|
-
if (specification.sampleRate <= 0 || specification.channels <= 0 || specification.bitDepth === void 0) {
|
|
576
|
-
throw new Error(`RAW audio format "${specification.format}" does not define a complete audio specification.`);
|
|
577
|
-
}
|
|
578
|
-
if (specification.codec === "siren") return;
|
|
579
|
-
if (specification.codec === "silk") {
|
|
580
|
-
if (buffer.byteLength <= 9 || !ascii(new Uint8Array(buffer), 0, "#!SILK_V3"))
|
|
581
|
-
throw new Error("RAW SILK audio must contain a valid #!SILK_V3 payload header.");
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (specification.codec === "opus" && buffer.byteLength === 0) throw new Error("RAW Opus audio cannot be empty.");
|
|
585
|
-
if (specification.codec === "opus") {
|
|
586
|
-
const packetCode = new Uint8Array(buffer)[0] ?? 0;
|
|
587
|
-
const frameCountCode = packetCode & 3;
|
|
588
|
-
if (packetCode >> 3 > 31 || buffer.byteLength < (frameCountCode === 3 ? 2 : 2) || frameCountCode === 3 && ((new Uint8Array(buffer)[1] ?? 0) & 63) === 0)
|
|
589
|
-
throw new Error("RAW Opus audio has an invalid packet framing header.");
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const bytesPerFrame = specification.channels * Math.ceil(specification.bitDepth / 8);
|
|
593
|
-
if (bytesPerFrame <= 0 || buffer.byteLength % bytesPerFrame !== 0) {
|
|
594
|
-
throw new Error(
|
|
595
|
-
`RAW audio buffer size ${buffer.byteLength} is not aligned to ${bytesPerFrame}-byte audio frames for "${specification.format}".`
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function validateContainerFormat(specification, format) {
|
|
600
|
-
const expected = formatAudioSpecification(format);
|
|
601
|
-
if (expected.sampleRate > 0 && specification.sampleRate !== expected.sampleRate || expected.channels > 0 && specification.channels !== expected.channels || expected.codec !== "unknown" && specification.codec !== expected.codec) {
|
|
602
|
-
throw new AudioFormatMismatchError(`Audio container does not match the requested format "${format}".`, [
|
|
603
|
-
expected,
|
|
604
|
-
specification
|
|
605
|
-
]);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
function validateAudioSpecifications(specs) {
|
|
609
|
-
const first = specs[0];
|
|
610
|
-
if (!first) return;
|
|
611
|
-
const mismatch = specs.find(
|
|
612
|
-
(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
|
|
613
|
-
);
|
|
614
|
-
if (mismatch)
|
|
615
|
-
throw new AudioFormatMismatchError(
|
|
616
|
-
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
617
|
-
specs
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
function isAudioFormatMismatch(error) {
|
|
621
|
-
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
622
|
-
}
|
|
623
|
-
function writeUint32(target, offset, value) {
|
|
624
|
-
new DataView(target.buffer).setUint32(offset, value, true);
|
|
625
|
-
}
|
|
626
|
-
function writeChunk(target, offset, id, data) {
|
|
627
|
-
for (let index = 0; index < 4; index += 1) target[offset + index] = id.charCodeAt(index) ?? 0;
|
|
628
|
-
writeUint32(target, offset + 4, data.byteLength);
|
|
629
|
-
target.set(data, offset + 8);
|
|
630
|
-
const end = offset + 8 + data.byteLength;
|
|
631
|
-
if (data.byteLength & 1) target[end] = 0;
|
|
632
|
-
return end + (data.byteLength & 1);
|
|
633
|
-
}
|
|
634
|
-
function mergeWavBuffers(buffers) {
|
|
635
|
-
if (buffers.length === 0) return new ArrayBuffer(0);
|
|
636
|
-
const parsed = buffers.map(parseWav);
|
|
637
|
-
const first = parsed[0];
|
|
638
|
-
if (!first) throw new Error("At least one WAV buffer is required.");
|
|
639
|
-
if (parsed.some(
|
|
640
|
-
(item) => item.format.length !== first.format.length || item.format.some((value, i) => value !== first.format[i])
|
|
641
|
-
))
|
|
642
|
-
throw new Error("WAV buffers have incompatible fmt chunks.");
|
|
643
|
-
const dataLength = parsed.reduce((total, item) => total + item.data.byteLength, 0);
|
|
644
|
-
const nonDataLength = first.chunks.reduce(
|
|
645
|
-
(total, chunk) => chunk.id === "data" ? total : total + 8 + chunk.data.byteLength + (chunk.data.byteLength & 1),
|
|
646
|
-
0
|
|
647
|
-
);
|
|
648
|
-
const outputLength = 12 + nonDataLength + 8 + dataLength + (dataLength & 1);
|
|
649
|
-
if (outputLength - 8 > 4294967295) throw new RangeError("Merged WAV exceeds the RIFF format size limit.");
|
|
650
|
-
const output = new Uint8Array(outputLength);
|
|
651
|
-
output.set(Uint8Array.from([82, 73, 70, 70]), 0);
|
|
652
|
-
writeUint32(output, 4, outputLength - 8);
|
|
653
|
-
output.set(Uint8Array.from([87, 65, 86, 69]), 8);
|
|
654
|
-
let outputOffset = 12;
|
|
655
|
-
let dataWritten = false;
|
|
656
|
-
for (const chunk of first.chunks) {
|
|
657
|
-
if (chunk.id === "data") {
|
|
658
|
-
if (dataWritten) continue;
|
|
659
|
-
const data = new Uint8Array(dataLength);
|
|
660
|
-
let dataOffset = 0;
|
|
661
|
-
for (const item of parsed) {
|
|
662
|
-
data.set(item.data, dataOffset);
|
|
663
|
-
dataOffset += item.data.byteLength;
|
|
664
|
-
}
|
|
665
|
-
outputOffset = writeChunk(output, outputOffset, "data", data);
|
|
666
|
-
dataWritten = true;
|
|
667
|
-
} else {
|
|
668
|
-
outputOffset = writeChunk(output, outputOffset, chunk.id, chunk.data);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (!dataWritten) throw new Error("WAV audio must contain a data chunk.");
|
|
672
|
-
return output.buffer;
|
|
673
|
-
}
|
|
674
|
-
function skipId3v2(bytes) {
|
|
675
|
-
if (!ascii(bytes, 0, "ID3") || bytes.byteLength < 10) return 0;
|
|
676
|
-
const size = [bytes[6], bytes[7], bytes[8], bytes[9]].reduce((total, value) => total << 7 | value & 127, 0);
|
|
677
|
-
const hasFooter = (bytes[5] & 16) !== 0;
|
|
678
|
-
return Math.min(bytes.byteLength, 10 + size + (hasFooter ? 10 : 0));
|
|
679
|
-
}
|
|
680
|
-
function stripMp3Tags(buffer) {
|
|
681
|
-
const bytes = new Uint8Array(buffer);
|
|
682
|
-
const start = skipId3v2(bytes);
|
|
683
|
-
const end = bytes.byteLength >= 128 && ascii(bytes, bytes.byteLength - 128, "TAG") ? bytes.byteLength - 128 : bytes.byteLength;
|
|
684
|
-
return bytes.slice(Math.min(start, end), end);
|
|
685
|
-
}
|
|
686
|
-
function isMp3Format(format) {
|
|
687
|
-
return /(?:mp3|mpeg)/i.test(format);
|
|
688
|
-
}
|
|
689
|
-
function isOggFormat(format) {
|
|
690
|
-
return /ogg/i.test(format);
|
|
691
|
-
}
|
|
692
|
-
function isWebmFormat(format) {
|
|
693
|
-
return /webm/i.test(format);
|
|
694
|
-
}
|
|
695
|
-
function isWavFormat(format) {
|
|
696
|
-
return /(?:wav|wave|riff)/i.test(format);
|
|
697
|
-
}
|
|
698
|
-
function isRawFormat(format) {
|
|
699
|
-
return /^raw(?:-|$)/i.test(format);
|
|
700
|
-
}
|
|
701
|
-
function validateMergedAudioBuffer(merged, format, buffers, inputSpecs, outputMimeType, allowExternalContainer) {
|
|
702
|
-
if (!(merged instanceof ArrayBuffer) || merged.byteLength === 0) {
|
|
703
|
-
throw new MergeError("The custom audio merger returned an empty or invalid audio buffer.");
|
|
704
|
-
}
|
|
705
|
-
let specification;
|
|
706
|
-
try {
|
|
707
|
-
specification = inspectAudioSpecification(merged, format);
|
|
708
|
-
} catch (error) {
|
|
709
|
-
if (!allowExternalContainer) throw error;
|
|
710
|
-
specification = inputSpecs[0] ?? formatAudioSpecification(format);
|
|
711
|
-
}
|
|
712
|
-
if (!outputMimeType.trim()) throw new MergeError("The custom audio merger output MIME type cannot be empty.");
|
|
713
|
-
const firstInput = inputSpecs[0];
|
|
714
|
-
if (firstInput && (specification.sampleRate !== firstInput.sampleRate || specification.channels !== firstInput.channels || specification.codec !== firstInput.codec || firstInput.bitDepth !== void 0 && specification.bitDepth !== firstInput.bitDepth)) {
|
|
715
|
-
throw new AudioFormatMismatchError("The custom audio merger returned an incompatible audio stream.", [
|
|
716
|
-
...inputSpecs,
|
|
717
|
-
specification
|
|
718
|
-
]);
|
|
719
|
-
}
|
|
720
|
-
if (isRawFormat(format)) {
|
|
721
|
-
const expectedSize = buffers.reduce((total, input) => total + input.byteLength, 0);
|
|
722
|
-
if (merged.byteLength !== expectedSize) {
|
|
723
|
-
throw new MergeError(
|
|
724
|
-
`The custom audio merger returned ${merged.byteLength} bytes; ${expectedSize} were expected.`
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
validateRawAudioBuffer(merged, specification);
|
|
728
|
-
}
|
|
729
|
-
return specification;
|
|
730
|
-
}
|
|
731
|
-
async function withinDeadline(value, deadline) {
|
|
732
|
-
if (!deadline) return value;
|
|
733
|
-
deadline.throwIfExpired();
|
|
734
|
-
if (!Number.isFinite(deadline.remainingMs)) return value;
|
|
735
|
-
let timer;
|
|
736
|
-
try {
|
|
737
|
-
return await Promise.race([
|
|
738
|
-
Promise.resolve(value),
|
|
739
|
-
new Promise((_resolve, reject) => {
|
|
740
|
-
timer = setTimeout(
|
|
741
|
-
() => reject(new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.")),
|
|
742
|
-
deadline.remainingMs
|
|
743
|
-
);
|
|
744
|
-
})
|
|
745
|
-
]);
|
|
746
|
-
} finally {
|
|
747
|
-
if (timer) clearTimeout(timer);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
function resolveMergeAudioFormat(format) {
|
|
751
|
-
if (isWavFormat(format)) return "wav";
|
|
752
|
-
if (isMp3Format(format)) return "mp3";
|
|
753
|
-
if (isRawFormat(format)) return "raw";
|
|
754
|
-
return void 0;
|
|
755
|
-
}
|
|
756
|
-
function canMergeAudioFormat(format) {
|
|
757
|
-
return resolveMergeAudioFormat(format) !== void 0;
|
|
758
|
-
}
|
|
759
|
-
function mergeAudioBuffers(buffers, options) {
|
|
760
|
-
const format = typeof options === "string" ? options : options?.format;
|
|
761
|
-
if (!format) throw new UnsupportedMergeFormatError("");
|
|
762
|
-
if (!canMergeAudioFormat(format)) throw new UnsupportedMergeFormatError(format);
|
|
763
|
-
try {
|
|
764
|
-
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
765
|
-
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
766
|
-
if (isMp3Format(format)) {
|
|
767
|
-
const parts = buffers.map(stripMp3Tags);
|
|
768
|
-
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
769
|
-
let offset = 0;
|
|
770
|
-
for (const part of parts) {
|
|
771
|
-
output.set(part, offset);
|
|
772
|
-
offset += part.byteLength;
|
|
773
|
-
}
|
|
774
|
-
return output.buffer;
|
|
775
|
-
}
|
|
776
|
-
if (isRawFormat(format)) {
|
|
777
|
-
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
778
|
-
let offset = 0;
|
|
779
|
-
for (const buffer of buffers) {
|
|
780
|
-
output.set(new Uint8Array(buffer), offset);
|
|
781
|
-
offset += buffer.byteLength;
|
|
782
|
-
}
|
|
783
|
-
return output.buffer;
|
|
784
|
-
}
|
|
785
|
-
throw new UnsupportedMergeFormatError(format);
|
|
786
|
-
} catch (error) {
|
|
787
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
788
|
-
throw error;
|
|
789
|
-
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function closeSpeechResources(speechConfig, synthesizer) {
|
|
793
|
-
try {
|
|
794
|
-
synthesizer.close();
|
|
795
|
-
} catch {
|
|
796
|
-
}
|
|
797
|
-
try {
|
|
798
|
-
speechConfig.close();
|
|
799
|
-
} catch {
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
803
|
-
async function synthesizeSsmlOnce(ssml, config) {
|
|
804
|
-
if (config.signal?.aborted) {
|
|
805
|
-
throw new SynthesisCancelledError();
|
|
806
|
-
}
|
|
807
|
-
const speechConfig = createSpeechConfig(config);
|
|
808
|
-
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
809
|
-
return await new Promise((resolve, reject) => {
|
|
810
|
-
let resourcesClosed = false;
|
|
811
|
-
let settled = false;
|
|
812
|
-
let timeout;
|
|
813
|
-
let abortHandler;
|
|
814
|
-
const cleanup = () => {
|
|
815
|
-
if (timeout) clearTimeout(timeout);
|
|
816
|
-
if (abortHandler) config.signal?.removeEventListener("abort", abortHandler);
|
|
817
|
-
};
|
|
818
|
-
const closeResources = () => {
|
|
819
|
-
if (resourcesClosed) return;
|
|
820
|
-
resourcesClosed = true;
|
|
821
|
-
closeSpeechResources(speechConfig, synthesizer);
|
|
822
|
-
};
|
|
823
|
-
const rejectWithError = (error) => {
|
|
824
|
-
if (settled) return;
|
|
825
|
-
settled = true;
|
|
826
|
-
cleanup();
|
|
827
|
-
closeResources();
|
|
828
|
-
reject(toSynthesisError(error));
|
|
829
|
-
};
|
|
830
|
-
const boundaries = [];
|
|
831
|
-
const visemes = [];
|
|
832
|
-
const bookmarks = [];
|
|
833
|
-
let sourceEventCursor = 0;
|
|
834
|
-
let generatedSourceMap;
|
|
835
|
-
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
836
|
-
try {
|
|
837
|
-
generatedSourceMap = getSsmlSourceMap(ssml);
|
|
838
|
-
} catch {
|
|
839
|
-
generatedSourceMap = void 0;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
843
|
-
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
844
|
-
...segment,
|
|
845
|
-
range: {
|
|
846
|
-
start: segment.range.start + sourceBaseOffset,
|
|
847
|
-
end: segment.range.end + sourceBaseOffset
|
|
848
|
-
},
|
|
849
|
-
sourceNodePath: [...segment.sourceNodePath]
|
|
850
|
-
})) ?? [];
|
|
851
|
-
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
852
|
-
...marker,
|
|
853
|
-
originalTextRange: {
|
|
854
|
-
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
855
|
-
end: marker.originalTextRange.end + sourceBaseOffset
|
|
856
|
-
},
|
|
857
|
-
sourceNodePath: [...marker.sourceNodePath]
|
|
858
|
-
})) ?? [];
|
|
859
|
-
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
860
|
-
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
861
|
-
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
862
|
-
if (marker) {
|
|
863
|
-
return {
|
|
864
|
-
originalTextRange: { ...marker.originalTextRange },
|
|
865
|
-
sourceNodePath: [...marker.sourceNodePath],
|
|
866
|
-
textRange: { ...marker.originalTextRange },
|
|
867
|
-
mappingStatus: "exact"
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
871
|
-
return { mappingStatus: "unmapped" };
|
|
872
|
-
}
|
|
873
|
-
const value = text ?? "";
|
|
874
|
-
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
875
|
-
let mappingStatus = "exact";
|
|
876
|
-
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
877
|
-
localStart = -1;
|
|
878
|
-
mappingStatus = "fallback";
|
|
879
|
-
}
|
|
880
|
-
if (localStart < 0 || localStart > sourceText.length) {
|
|
881
|
-
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
882
|
-
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
883
|
-
mappingStatus = "fallback";
|
|
884
|
-
}
|
|
885
|
-
localStart = Math.max(0, localStart);
|
|
886
|
-
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
887
|
-
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
888
|
-
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
889
|
-
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
890
|
-
const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
|
|
891
|
-
return {
|
|
892
|
-
originalTextRange: { ...fallbackRange },
|
|
893
|
-
textRange: { ...fallbackRange },
|
|
894
|
-
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
895
|
-
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
896
|
-
};
|
|
897
|
-
};
|
|
898
|
-
synthesizer.wordBoundary = (_sender, event) => {
|
|
899
|
-
boundaries.push({
|
|
900
|
-
text: event.text,
|
|
901
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
902
|
-
durationMs: ticksToMilliseconds(event.duration),
|
|
903
|
-
...mapSourceEvent(
|
|
904
|
-
event.text,
|
|
905
|
-
event.textOffset
|
|
906
|
-
)
|
|
907
|
-
});
|
|
908
|
-
};
|
|
909
|
-
synthesizer.visemeReceived = (_sender, event) => {
|
|
910
|
-
const eventWithOffset = event;
|
|
911
|
-
visemes.push({
|
|
912
|
-
visemeId: event.visemeId,
|
|
913
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
914
|
-
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
915
|
-
});
|
|
916
|
-
};
|
|
917
|
-
synthesizer.bookmarkReached = (_sender, event) => {
|
|
918
|
-
const eventWithOffset = event;
|
|
919
|
-
bookmarks.push({
|
|
920
|
-
name: event.text,
|
|
921
|
-
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
922
|
-
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
923
|
-
});
|
|
924
|
-
};
|
|
925
|
-
const cb = (result) => {
|
|
926
|
-
if (settled) return;
|
|
927
|
-
const { reason, errorDetails } = result;
|
|
928
|
-
if (reason !== SpeechSDK2.ResultReason.SynthesizingAudioCompleted) {
|
|
929
|
-
const err = errorDetails || `Speech synthesis failed with reason ${reason}.`;
|
|
930
|
-
rejectWithError(err);
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
let audioSpec;
|
|
934
|
-
try {
|
|
935
|
-
audioSpec = inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT);
|
|
936
|
-
} catch (error) {
|
|
937
|
-
rejectWithError(error);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
settled = true;
|
|
941
|
-
cleanup();
|
|
942
|
-
closeResources();
|
|
943
|
-
const eventDurationMs = Math.max(
|
|
944
|
-
0,
|
|
945
|
-
...(boundaries ?? []).map((boundary) => boundary.audioOffsetMs + boundary.durationMs),
|
|
946
|
-
...(visemes ?? []).map((viseme) => viseme.audioOffsetMs),
|
|
947
|
-
...(bookmarks ?? []).map((bookmark) => bookmark.audioOffsetMs)
|
|
948
|
-
);
|
|
949
|
-
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
950
|
-
const requestId = result.resultId;
|
|
951
|
-
const addSourceMetadata = (event) => {
|
|
952
|
-
const mapped = {
|
|
953
|
-
...event,
|
|
954
|
-
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
955
|
-
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
956
|
-
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
957
|
-
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
958
|
-
...requestId ? { requestId } : {}
|
|
959
|
-
};
|
|
960
|
-
if (event.mappingStatus === "unmapped") {
|
|
961
|
-
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
962
|
-
Object.defineProperty(mapped, "toJSON", {
|
|
963
|
-
value: () => ({ ...mapped, mappingStatus: "unmapped" }),
|
|
964
|
-
enumerable: false
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
return mapped;
|
|
968
|
-
};
|
|
969
|
-
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
970
|
-
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
971
|
-
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
972
|
-
resolve({
|
|
973
|
-
audioData: result.audioData,
|
|
974
|
-
durationMs,
|
|
975
|
-
audioSpec,
|
|
976
|
-
mimeType: config.outputMimeType ?? resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
977
|
-
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
978
|
-
...requestId ? { requestId } : {},
|
|
979
|
-
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
980
|
-
...sourceVisemes.length > 0 ? { visemes: sourceVisemes } : {},
|
|
981
|
-
...sourceBookmarks.length > 0 ? { bookmarks: sourceBookmarks } : {}
|
|
982
|
-
});
|
|
983
|
-
};
|
|
984
|
-
try {
|
|
985
|
-
if (config.signal) {
|
|
986
|
-
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
987
|
-
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
988
|
-
}
|
|
989
|
-
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeouts?.totalJobMs ?? config.timeoutMs;
|
|
990
|
-
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
991
|
-
timeout = setTimeout(
|
|
992
|
-
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
993
|
-
timeoutMs
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
997
|
-
} catch (error) {
|
|
998
|
-
rejectWithError(error);
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
function isRetryableSynthesisError(error) {
|
|
1003
|
-
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
1004
|
-
if (error instanceof AzureTtsError && error.status !== 0)
|
|
1005
|
-
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
1006
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
-
if (/\b4\d{2}\b/.test(message)) return false;
|
|
1008
|
-
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
1009
|
-
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
1010
|
-
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
1011
|
-
}
|
|
1012
|
-
function retryDelay(options, retryAttempt, error) {
|
|
1013
|
-
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1014
|
-
if (retryAfterMs !== void 0) return retryAfterMs;
|
|
1015
|
-
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
1016
|
-
return Math.floor(Math.random() * (base + 1));
|
|
1017
|
-
}
|
|
1018
|
-
function resolveConcurrency(value, total) {
|
|
1019
|
-
if (value === void 0) return 1;
|
|
1020
|
-
if (value === Infinity) return Math.max(1, total);
|
|
1021
|
-
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1022
|
-
}
|
|
1023
|
-
async function waitForRetry(delayMs, signal) {
|
|
1024
|
-
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
1025
|
-
if (delayMs <= 0) return;
|
|
1026
|
-
await new Promise((resolve, reject) => {
|
|
1027
|
-
let timer;
|
|
1028
|
-
const abort = () => {
|
|
1029
|
-
clearTimeout(timer);
|
|
1030
|
-
signal?.removeEventListener("abort", abort);
|
|
1031
|
-
reject(new SynthesisCancelledError());
|
|
1032
|
-
};
|
|
1033
|
-
timer = setTimeout(() => {
|
|
1034
|
-
signal?.removeEventListener("abort", abort);
|
|
1035
|
-
resolve();
|
|
1036
|
-
}, delayMs);
|
|
1037
|
-
if (signal) {
|
|
1038
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry, deadlineAtMs) {
|
|
1043
|
-
const options = retryOptions ? {
|
|
1044
|
-
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
1045
|
-
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
1046
|
-
maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
|
|
1047
|
-
shouldRetry: retryOptions.shouldRetry
|
|
1048
|
-
} : void 0;
|
|
1049
|
-
let attempt = 0;
|
|
1050
|
-
while (true) {
|
|
1051
|
-
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
1052
|
-
try {
|
|
1053
|
-
return await synthesizeSsmlOnce(ssml, config);
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
1056
|
-
throw error;
|
|
1057
|
-
attempt += 1;
|
|
1058
|
-
const delayMs = retryDelay(options, attempt, error);
|
|
1059
|
-
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1060
|
-
if (getRetryAfterDelayMs(error) !== void 0 && (delayMs > options.maxDelayMs || remainingMs !== void 0 && delayMs > remainingMs)) {
|
|
1061
|
-
throw new SynthesisTimeoutError(
|
|
1062
|
-
remainingMs === void 0 ? `Retry-After exceeded maxDelayMs (${options.maxDelayMs} ms).` : `Retry-After exceeded the remaining total job timeout (${remainingMs} ms).`
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
onRetry(attempt, delayMs);
|
|
1066
|
-
await waitForRetry(Math.min(delayMs, remainingMs ?? delayMs), config.signal);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
async function synthesizeSsml(ssml, config) {
|
|
1071
|
-
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1072
|
-
try {
|
|
1073
|
-
deadline.throwIfExpired();
|
|
1074
|
-
const synthesisConfig = {
|
|
1075
|
-
...config,
|
|
1076
|
-
signal: deadline.signal,
|
|
1077
|
-
timeouts: config.timeouts ? { ...config.timeouts, totalJobMs: void 0 } : void 0
|
|
1078
|
-
};
|
|
1079
|
-
const result = config.retryOptions ? await synthesizeWithRetry(ssml, synthesisConfig, config.retryOptions, () => void 0, deadline.deadlineAtMs) : await synthesizeSsmlOnce(ssml, synthesisConfig);
|
|
1080
|
-
deadline.throwIfExpired();
|
|
1081
|
-
return result;
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
if (deadline.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
1084
|
-
throw error;
|
|
1085
|
-
} finally {
|
|
1086
|
-
deadline.dispose();
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
function createAbortScope(parent, timeoutMs) {
|
|
1090
|
-
const controller = new AbortController();
|
|
1091
|
-
let didTimeout = false;
|
|
1092
|
-
const onAbort = () => controller.abort();
|
|
1093
|
-
if (parent?.aborted) controller.abort();
|
|
1094
|
-
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1095
|
-
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
1096
|
-
didTimeout = true;
|
|
1097
|
-
controller.abort();
|
|
1098
|
-
}, timeoutMs) : void 0;
|
|
1099
|
-
return {
|
|
1100
|
-
signal: controller.signal,
|
|
1101
|
-
timedOut: () => didTimeout,
|
|
1102
|
-
dispose: () => {
|
|
1103
|
-
if (timer) clearTimeout(timer);
|
|
1104
|
-
parent?.removeEventListener("abort", onAbort);
|
|
1105
|
-
},
|
|
1106
|
-
abort: () => controller.abort()
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry, deadlineAtMs) {
|
|
1110
|
-
const scope = createAbortScope(config.signal, timeoutMs);
|
|
1111
|
-
try {
|
|
1112
|
-
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry, deadlineAtMs);
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
1115
|
-
throw error;
|
|
1116
|
-
} finally {
|
|
1117
|
-
scope.dispose();
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
async function synthesizeSsmlChunks(chunks, config) {
|
|
1121
|
-
const totalChunks = chunks.length;
|
|
1122
|
-
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1123
|
-
const fingerprints = inputs.map(
|
|
1124
|
-
(chunk) => computeChunkFingerprint(chunk.ssml, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT, {
|
|
1125
|
-
region: config.region,
|
|
1126
|
-
endpoint: config.endpoint,
|
|
1127
|
-
customHeaders: config.customHeaders,
|
|
1128
|
-
fingerprintSchemaVersion: config.fingerprintSchemaVersion
|
|
1129
|
-
})
|
|
1130
|
-
);
|
|
1131
|
-
const results = new Array(totalChunks);
|
|
1132
|
-
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1133
|
-
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1134
|
-
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1135
|
-
chunkIndex,
|
|
1136
|
-
status: "pending",
|
|
1137
|
-
canResume: true
|
|
1138
|
-
}));
|
|
1139
|
-
for (const [index, cached] of cachedChunks) {
|
|
1140
|
-
if (index < 0 || index >= totalChunks) continue;
|
|
1141
|
-
const isValid = config.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index];
|
|
1142
|
-
if (isValid) {
|
|
1143
|
-
results[index] = { ...cached };
|
|
1144
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1145
|
-
} else {
|
|
1146
|
-
invalidCachedIndices.add(index);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
1150
|
-
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1151
|
-
const jobStartedAt = Date.now();
|
|
1152
|
-
const jobDeadlineAt = config.timeouts?.totalJobMs !== void 0 && config.timeouts.totalJobMs > 0 ? jobStartedAt + config.timeouts.totalJobMs : void 0;
|
|
1153
|
-
const deadline = new DeadlineController(config.timeouts?.totalJobMs, config.signal);
|
|
1154
|
-
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
1155
|
-
const report = (event) => config.onProgress?.(event);
|
|
1156
|
-
for (const [index, input] of inputs.entries()) {
|
|
1157
|
-
report({
|
|
1158
|
-
currentChunk: index,
|
|
1159
|
-
totalChunks,
|
|
1160
|
-
percent: totalChunks === 0 ? 100 : Math.round(index / totalChunks * 100),
|
|
1161
|
-
chunkIndex: index,
|
|
1162
|
-
originalTextRange: input.originalTextRange,
|
|
1163
|
-
status: "pending",
|
|
1164
|
-
durationMs: 0
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
let completed = [...results].filter((result) => result !== void 0).length;
|
|
1168
|
-
let nextIndex = 0;
|
|
1169
|
-
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
1170
|
-
let firstError;
|
|
1171
|
-
const failedIndices = /* @__PURE__ */ new Set();
|
|
1172
|
-
const worker = async () => {
|
|
1173
|
-
while (true) {
|
|
1174
|
-
const index = nextIndex++;
|
|
1175
|
-
if (index >= chunks.length) return;
|
|
1176
|
-
if (!shouldSynthesize(index)) continue;
|
|
1177
|
-
if (firstError && config.cancelOnFailure !== false) return;
|
|
1178
|
-
const input = inputs[index];
|
|
1179
|
-
report({
|
|
1180
|
-
currentChunk: completed,
|
|
1181
|
-
totalChunks,
|
|
1182
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
1183
|
-
chunkIndex: index,
|
|
1184
|
-
originalTextRange: input.originalTextRange,
|
|
1185
|
-
status: "synthesizing",
|
|
1186
|
-
durationMs: 0
|
|
1187
|
-
});
|
|
1188
|
-
const startedAt = Date.now();
|
|
1189
|
-
try {
|
|
1190
|
-
const result = await synthesizeChunkWithTimeout(
|
|
1191
|
-
input.ssml,
|
|
1192
|
-
{
|
|
1193
|
-
...config,
|
|
1194
|
-
signal: deadline.signal,
|
|
1195
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
1196
|
-
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
1197
|
-
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
1198
|
-
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
1199
|
-
chunkIndex: index,
|
|
1200
|
-
onProgress: void 0
|
|
1201
|
-
},
|
|
1202
|
-
config.retryOptions,
|
|
1203
|
-
config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
|
|
1204
|
-
(retryAttempt, nextRetryDelayMs) => report({
|
|
1205
|
-
currentChunk: completed,
|
|
1206
|
-
totalChunks,
|
|
1207
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
1208
|
-
chunkIndex: index,
|
|
1209
|
-
originalTextRange: input.originalTextRange,
|
|
1210
|
-
status: "synthesizing",
|
|
1211
|
-
durationMs: Date.now() - startedAt,
|
|
1212
|
-
retryAttempt,
|
|
1213
|
-
nextRetryDelayMs,
|
|
1214
|
-
isRetrying: true
|
|
1215
|
-
}),
|
|
1216
|
-
jobDeadlineAt
|
|
1217
|
-
);
|
|
1218
|
-
results[index] = result;
|
|
1219
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result };
|
|
1220
|
-
completed += 1;
|
|
1221
|
-
report({
|
|
1222
|
-
currentChunk: completed,
|
|
1223
|
-
totalChunks,
|
|
1224
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
1225
|
-
chunkIndex: index,
|
|
1226
|
-
originalTextRange: input.originalTextRange,
|
|
1227
|
-
status: "success",
|
|
1228
|
-
durationMs: Date.now() - startedAt
|
|
1229
|
-
});
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
const wasCancelled = firstError !== void 0 || scope.signal.aborted && !scope.timedOut();
|
|
1232
|
-
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
1233
|
-
if (!wasCancelled) failedIndices.add(index);
|
|
1234
|
-
chunkStates[index] = {
|
|
1235
|
-
chunkIndex: index,
|
|
1236
|
-
status: wasCancelled ? "cancelled" : "failed",
|
|
1237
|
-
isOriginalFailure: !wasCancelled,
|
|
1238
|
-
canResume: true,
|
|
1239
|
-
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1240
|
-
};
|
|
1241
|
-
report({
|
|
1242
|
-
currentChunk: completed,
|
|
1243
|
-
totalChunks,
|
|
1244
|
-
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
1245
|
-
chunkIndex: index,
|
|
1246
|
-
originalTextRange: input.originalTextRange,
|
|
1247
|
-
status: "failed",
|
|
1248
|
-
durationMs: Date.now() - startedAt,
|
|
1249
|
-
error
|
|
1250
|
-
});
|
|
1251
|
-
if (config.cancelOnFailure !== false) scope.abort();
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
};
|
|
1256
|
-
try {
|
|
1257
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1258
|
-
if (firstError) throw firstError;
|
|
1259
|
-
const missingChunkIndices = Array.from(
|
|
1260
|
-
{ length: totalChunks },
|
|
1261
|
-
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1262
|
-
).filter((index) => index !== void 0);
|
|
1263
|
-
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(totalChunks, missingChunkIndices);
|
|
1264
|
-
const orderedResults = results.filter((result) => result !== void 0);
|
|
1265
|
-
return await mergeSynthesisResults(orderedResults, {
|
|
1266
|
-
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
1267
|
-
signal: deadline.signal,
|
|
1268
|
-
customMerger: config.customMerger,
|
|
1269
|
-
outputMimeType: config.outputMimeType,
|
|
1270
|
-
postMergeValidator: config.postMergeValidator,
|
|
1271
|
-
deadline
|
|
1272
|
-
});
|
|
1273
|
-
} catch (error) {
|
|
1274
|
-
if (firstError && config.cancelOnFailure !== false) {
|
|
1275
|
-
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1276
|
-
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1277
|
-
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
const synthesizedChunks = results.flatMap(
|
|
1282
|
-
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1283
|
-
);
|
|
1284
|
-
const partial = {
|
|
1285
|
-
synthesizedChunks,
|
|
1286
|
-
completedChunks: synthesizedChunks,
|
|
1287
|
-
pendingChunkIndices: chunkStates.flatMap(
|
|
1288
|
-
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1289
|
-
),
|
|
1290
|
-
failedChunkIndices: [...failedIndices],
|
|
1291
|
-
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1292
|
-
chunkStates,
|
|
1293
|
-
totalChunks
|
|
1294
|
-
};
|
|
1295
|
-
if (error && typeof error === "object") error.partialResult = partial;
|
|
1296
|
-
throw error;
|
|
1297
|
-
} finally {
|
|
1298
|
-
scope.dispose();
|
|
1299
|
-
deadline.dispose();
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
1303
|
-
const boundaries = [];
|
|
1304
|
-
const visemes = [];
|
|
1305
|
-
const bookmarks = [];
|
|
1306
|
-
let durationOffset = 0;
|
|
1307
|
-
for (const [resultIndex, result] of results.entries()) {
|
|
1308
|
-
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
1309
|
-
for (const boundary of chunkBoundaries) {
|
|
1310
|
-
const textRange = boundary.textRange ?? result.textRange;
|
|
1311
|
-
const originalTextRange = boundary.originalTextRange ?? textRange;
|
|
1312
|
-
const requestId = boundary.requestId ?? result.requestId;
|
|
1313
|
-
boundaries.push({
|
|
1314
|
-
...boundary,
|
|
1315
|
-
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
1316
|
-
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
1317
|
-
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1318
|
-
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
1319
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1320
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1321
|
-
...requestId ? { requestId } : {},
|
|
1322
|
-
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
for (const viseme of result.visemes ?? []) {
|
|
1326
|
-
const textRange = viseme.textRange ?? result.textRange;
|
|
1327
|
-
const originalTextRange = viseme.originalTextRange ?? textRange;
|
|
1328
|
-
const requestId = viseme.requestId ?? result.requestId;
|
|
1329
|
-
visemes.push({
|
|
1330
|
-
...viseme,
|
|
1331
|
-
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
1332
|
-
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
1333
|
-
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1334
|
-
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
1335
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1336
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1337
|
-
...requestId ? { requestId } : {},
|
|
1338
|
-
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
for (const bookmark of result.bookmarks ?? []) {
|
|
1342
|
-
const textRange = bookmark.textRange ?? result.textRange;
|
|
1343
|
-
const originalTextRange = bookmark.originalTextRange ?? textRange;
|
|
1344
|
-
const requestId = bookmark.requestId ?? result.requestId;
|
|
1345
|
-
bookmarks.push({
|
|
1346
|
-
...bookmark,
|
|
1347
|
-
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
1348
|
-
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
1349
|
-
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
1350
|
-
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
1351
|
-
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
1352
|
-
...textRange ? { textRange: { ...textRange } } : {},
|
|
1353
|
-
...requestId ? { requestId } : {},
|
|
1354
|
-
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
durationOffset += Math.max(0, result.durationMs);
|
|
1358
|
-
}
|
|
1359
|
-
return {
|
|
1360
|
-
audioData,
|
|
1361
|
-
durationMs: durationOffset,
|
|
1362
|
-
mimeType: resolveMimeType(format),
|
|
1363
|
-
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
1364
|
-
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
1365
|
-
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
1366
|
-
...visemes.length > 0 ? { visemes } : {},
|
|
1367
|
-
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
1368
|
-
...results.length === 1 && results[0]?.requestId ? { requestId: results[0].requestId } : {},
|
|
1369
|
-
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
function mergeSynthesisResults(results, options) {
|
|
1373
|
-
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
1374
|
-
const format = resolvedOptions?.format;
|
|
1375
|
-
if (!format) throw new UnsupportedMergeFormatError("");
|
|
1376
|
-
const buffers = results.map((result) => result.audioData);
|
|
1377
|
-
const inputSpecs = results.map((result) => {
|
|
1378
|
-
if (result.audioSpec) return result.audioSpec;
|
|
1379
|
-
try {
|
|
1380
|
-
return inspectAudioSpecification(result.audioData, format);
|
|
1381
|
-
} catch (error) {
|
|
1382
|
-
if (resolvedOptions.customMerger) return formatAudioSpecification(format);
|
|
1383
|
-
throw error;
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
validateAudioSpecifications(inputSpecs);
|
|
1387
|
-
const deadline = resolvedOptions.deadline;
|
|
1388
|
-
deadline?.throwIfExpired();
|
|
1389
|
-
const signal = resolvedOptions.signal ?? deadline?.signal ?? new AbortController().signal;
|
|
1390
|
-
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1391
|
-
if (resolvedOptions.customMerger) {
|
|
1392
|
-
return withinDeadline(
|
|
1393
|
-
Promise.resolve().then(
|
|
1394
|
-
() => resolvedOptions.customMerger?.(buffers, {
|
|
1395
|
-
format,
|
|
1396
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1397
|
-
inputSpecs,
|
|
1398
|
-
signal
|
|
1399
|
-
})
|
|
1400
|
-
),
|
|
1401
|
-
deadline
|
|
1402
|
-
).then((merged) => {
|
|
1403
|
-
deadline?.throwIfExpired();
|
|
1404
|
-
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
1405
|
-
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1406
|
-
const mergedSpec = validateMergedAudioBuffer(
|
|
1407
|
-
merged,
|
|
1408
|
-
format,
|
|
1409
|
-
buffers,
|
|
1410
|
-
inputSpecs,
|
|
1411
|
-
resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1412
|
-
true
|
|
1413
|
-
);
|
|
1414
|
-
const result = createMergedResult(results, merged, format, mergedSpec, resolvedOptions.outputMimeType);
|
|
1415
|
-
return Promise.resolve(
|
|
1416
|
-
resolvedOptions.postMergeValidator?.(result, {
|
|
1417
|
-
format,
|
|
1418
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1419
|
-
inputSpecs,
|
|
1420
|
-
signal
|
|
1421
|
-
})
|
|
1422
|
-
).then((valid) => {
|
|
1423
|
-
deadline?.throwIfExpired();
|
|
1424
|
-
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1425
|
-
return result;
|
|
1426
|
-
});
|
|
1427
|
-
}).catch((error) => {
|
|
1428
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1429
|
-
throw error;
|
|
1430
|
-
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
try {
|
|
1434
|
-
deadline?.throwIfExpired();
|
|
1435
|
-
const result = createMergedResult(
|
|
1436
|
-
results,
|
|
1437
|
-
mergeAudioBuffers(buffers, { format }),
|
|
1438
|
-
format,
|
|
1439
|
-
inputSpecs[0],
|
|
1440
|
-
resolvedOptions.outputMimeType
|
|
1441
|
-
);
|
|
1442
|
-
if (resolvedOptions.postMergeValidator) {
|
|
1443
|
-
const validation = resolvedOptions.postMergeValidator(result, {
|
|
1444
|
-
format,
|
|
1445
|
-
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1446
|
-
inputSpecs,
|
|
1447
|
-
signal
|
|
1448
|
-
});
|
|
1449
|
-
if (validation instanceof Promise)
|
|
1450
|
-
return withinDeadline(validation, deadline).then((valid) => {
|
|
1451
|
-
deadline?.throwIfExpired();
|
|
1452
|
-
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1453
|
-
return result;
|
|
1454
|
-
});
|
|
1455
|
-
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1456
|
-
}
|
|
1457
|
-
deadline?.throwIfExpired();
|
|
1458
|
-
return result;
|
|
1459
|
-
} catch (error) {
|
|
1460
|
-
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1461
|
-
throw error;
|
|
1462
|
-
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
async function synthesizeSpeech(ssml, config) {
|
|
1466
|
-
return (await synthesizeSsml(ssml, config)).audioData;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// src/safe.ts
|
|
1470
|
-
import {
|
|
1471
|
-
createAzureUrlValidatorRunner,
|
|
1472
|
-
validateAzureSsml
|
|
1473
|
-
} from "@ssml-builder-js/ssml-core";
|
|
1474
|
-
var ChunkValidationError = class extends Error {
|
|
1475
|
-
constructor(chunkIndex, diagnostics) {
|
|
1476
|
-
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
1477
|
-
this.kind = "validation-error";
|
|
1478
|
-
this.name = "ChunkValidationError";
|
|
1479
|
-
this.chunkIndex = chunkIndex;
|
|
1480
|
-
this.diagnostics = diagnostics;
|
|
1481
|
-
}
|
|
1482
|
-
};
|
|
1483
|
-
var BatchChunkValidationError = class extends ChunkValidationError {
|
|
1484
|
-
constructor(chunkDiagnostics) {
|
|
1485
|
-
const first = chunkDiagnostics[0];
|
|
1486
|
-
super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
|
|
1487
|
-
this.name = "BatchChunkValidationError";
|
|
1488
|
-
this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
|
|
1489
|
-
this.chunkDiagnostics = chunkDiagnostics;
|
|
1490
|
-
this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
|
|
1491
|
-
this.errorCount = this.totalErrorCount;
|
|
1492
|
-
this.totalErrors = this.totalErrorCount;
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
function failure(error, partialResult) {
|
|
1496
|
-
return {
|
|
1497
|
-
ok: false,
|
|
1498
|
-
success: false,
|
|
1499
|
-
status: error.kind,
|
|
1500
|
-
error,
|
|
1501
|
-
...partialResult ? { partialResult } : {}
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
function partialResultFrom(error) {
|
|
1505
|
-
if (!error || typeof error !== "object") return void 0;
|
|
1506
|
-
const partial = error.partialResult;
|
|
1507
|
-
if (!partial || typeof partial !== "object") return void 0;
|
|
1508
|
-
return partial;
|
|
1509
|
-
}
|
|
1510
|
-
function createSafeAbortScope(parent, timeoutMs) {
|
|
1511
|
-
const controller = new AbortController();
|
|
1512
|
-
let didTimeout = false;
|
|
1513
|
-
const onAbort = () => controller.abort();
|
|
1514
|
-
if (parent?.aborted) controller.abort();
|
|
1515
|
-
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1516
|
-
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
1517
|
-
didTimeout = true;
|
|
1518
|
-
controller.abort();
|
|
1519
|
-
}, timeoutMs) : void 0;
|
|
1520
|
-
return {
|
|
1521
|
-
signal: controller.signal,
|
|
1522
|
-
timedOut: () => didTimeout,
|
|
1523
|
-
dispose: () => {
|
|
1524
|
-
if (timer) clearTimeout(timer);
|
|
1525
|
-
parent?.removeEventListener("abort", onAbort);
|
|
1526
|
-
},
|
|
1527
|
-
abort: () => controller.abort()
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
function isRetryable(error) {
|
|
1531
|
-
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
1532
|
-
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
1533
|
-
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
1534
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1535
|
-
if (/\b4\d{2}\b/.test(message)) return false;
|
|
1536
|
-
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
1537
|
-
}
|
|
1538
|
-
function delayForRetry(options, attempt) {
|
|
1539
|
-
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
1540
|
-
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
1541
|
-
return Math.floor(Math.random() * (base + 1));
|
|
1542
|
-
}
|
|
1543
|
-
function retryDelayForError(options, attempt, error) {
|
|
1544
|
-
return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
|
|
1545
|
-
}
|
|
1546
|
-
function resolveConcurrency2(value, total) {
|
|
1547
|
-
if (value === void 0) return 1;
|
|
1548
|
-
if (value === Infinity) return Math.max(1, total);
|
|
1549
|
-
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1550
|
-
}
|
|
1551
|
-
async function retryableSynthesis(synthesize, options, signal, onRetry, deadlineAtMs) {
|
|
1552
|
-
const retry = options ? {
|
|
1553
|
-
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1554
|
-
initialDelayMs: options.initialDelayMs,
|
|
1555
|
-
maxDelayMs: options.maxDelayMs,
|
|
1556
|
-
shouldRetry: options.shouldRetry
|
|
1557
|
-
} : void 0;
|
|
1558
|
-
let attempt = 0;
|
|
1559
|
-
while (true) {
|
|
1560
|
-
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
1561
|
-
try {
|
|
1562
|
-
return await synthesize();
|
|
1563
|
-
} catch (error) {
|
|
1564
|
-
if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
|
|
1565
|
-
throw error;
|
|
1566
|
-
attempt += 1;
|
|
1567
|
-
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1568
|
-
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
1569
|
-
const remainingMs = deadlineAtMs === void 0 ? void 0 : Math.max(0, deadlineAtMs - Date.now());
|
|
1570
|
-
if (retryAfterMs !== void 0 && (retryAfterMs > retry.maxDelayMs || remainingMs !== void 0 && retryAfterMs > remainingMs)) {
|
|
1571
|
-
throw new Error("Speech synthesis timed out because Retry-After exceeded the available retry budget.");
|
|
1572
|
-
}
|
|
1573
|
-
onRetry(attempt, delayMs);
|
|
1574
|
-
if (delayMs > 0)
|
|
1575
|
-
await new Promise((resolve, reject) => {
|
|
1576
|
-
const timer = setTimeout(() => {
|
|
1577
|
-
signal?.removeEventListener("abort", abort);
|
|
1578
|
-
resolve();
|
|
1579
|
-
}, delayMs);
|
|
1580
|
-
const abort = () => {
|
|
1581
|
-
clearTimeout(timer);
|
|
1582
|
-
signal?.removeEventListener("abort", abort);
|
|
1583
|
-
reject(new Error("Speech synthesis was cancelled."));
|
|
1584
|
-
};
|
|
1585
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
function sharedValidationOptions(options, signal) {
|
|
1591
|
-
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1592
|
-
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
1593
|
-
const runner = createAzureUrlValidatorRunner(validator, {
|
|
1594
|
-
...options.urlValidation ?? {},
|
|
1595
|
-
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1596
|
-
...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1597
|
-
...signal ? { signal } : {},
|
|
1598
|
-
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1599
|
-
});
|
|
1600
|
-
return {
|
|
1601
|
-
...withValidationSignal(options, signal),
|
|
1602
|
-
urlValidatorRunner: runner
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
1606
|
-
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1607
|
-
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
1608
|
-
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
1609
|
-
if (deadline.signal.aborted) {
|
|
1610
|
-
const error = toSynthesisError(
|
|
1611
|
-
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1612
|
-
);
|
|
1613
|
-
deadline.dispose();
|
|
1614
|
-
return failure(error);
|
|
1615
|
-
}
|
|
1616
|
-
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1617
|
-
if (errors.length > 0) {
|
|
1618
|
-
deadline.dispose();
|
|
1619
|
-
return failure({
|
|
1620
|
-
kind: "validation-error",
|
|
1621
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
1622
|
-
diagnostics: errors
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
try {
|
|
1626
|
-
return {
|
|
1627
|
-
ok: true,
|
|
1628
|
-
success: true,
|
|
1629
|
-
status: "success",
|
|
1630
|
-
value: await client.synthesizeSsml(ssml, {
|
|
1631
|
-
signal: deadline.signal,
|
|
1632
|
-
timeoutMs: options.timeouts?.perChunkMs,
|
|
1633
|
-
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0
|
|
1634
|
-
})
|
|
1635
|
-
};
|
|
1636
|
-
} catch (error) {
|
|
1637
|
-
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
1638
|
-
const synthesisError = toSynthesisError(error);
|
|
1639
|
-
return failure(synthesisError);
|
|
1640
|
-
} finally {
|
|
1641
|
-
deadline.dispose();
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
1645
|
-
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
1646
|
-
const validationOptions = sharedValidationOptions(
|
|
1647
|
-
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1648
|
-
deadline.signal
|
|
1649
|
-
);
|
|
1650
|
-
if (deadline.signal.aborted) {
|
|
1651
|
-
const error = toSynthesisError(
|
|
1652
|
-
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled.")
|
|
1653
|
-
);
|
|
1654
|
-
deadline.dispose();
|
|
1655
|
-
return failure(error);
|
|
1656
|
-
}
|
|
1657
|
-
const pending = (index, status, error) => {
|
|
1658
|
-
options.onProgress?.({
|
|
1659
|
-
currentChunk: status === "success" ? index + 1 : index,
|
|
1660
|
-
totalChunks: chunks.length,
|
|
1661
|
-
percent: chunks.length === 0 ? 100 : Math.round((status === "success" ? index + 1 : index) / chunks.length * 100),
|
|
1662
|
-
chunkIndex: index,
|
|
1663
|
-
originalTextRange: typeof chunks[index] === "string" ? void 0 : chunks[index]?.originalTextRange,
|
|
1664
|
-
status,
|
|
1665
|
-
durationMs: 0,
|
|
1666
|
-
...error ? { error } : {}
|
|
1667
|
-
});
|
|
1668
|
-
};
|
|
1669
|
-
chunks.forEach((_chunk, index) => {
|
|
1670
|
-
pending(index, "pending");
|
|
1671
|
-
});
|
|
1672
|
-
const validations = await Promise.all(
|
|
1673
|
-
chunks.map(async (chunk, index) => {
|
|
1674
|
-
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
1675
|
-
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
1676
|
-
const diagnostics = await Promise.resolve(
|
|
1677
|
-
validateAzureSsml(ssml, {
|
|
1678
|
-
...validationOptions,
|
|
1679
|
-
...sourceNodePath ? { sourceNodePath } : {},
|
|
1680
|
-
chunkIndex: index
|
|
1681
|
-
})
|
|
1682
|
-
);
|
|
1683
|
-
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1684
|
-
})
|
|
1685
|
-
);
|
|
1686
|
-
if (deadline.signal.aborted) {
|
|
1687
|
-
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1688
|
-
deadline.dispose();
|
|
1689
|
-
return failure(error);
|
|
1690
|
-
}
|
|
1691
|
-
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1692
|
-
if (chunkDiagnostics.length > 0) {
|
|
1693
|
-
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1694
|
-
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1695
|
-
deadline.dispose();
|
|
1696
|
-
return failure(error);
|
|
1697
|
-
}
|
|
1698
|
-
let fallbackJobScope;
|
|
1699
|
-
try {
|
|
1700
|
-
if (client.synthesizeChunks) {
|
|
1701
|
-
const normalizedChunks = chunks.map((chunk) => {
|
|
1702
|
-
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
1703
|
-
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
1704
|
-
});
|
|
1705
|
-
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1706
|
-
onProgress: options.onProgress,
|
|
1707
|
-
outputFormat: options.outputFormat,
|
|
1708
|
-
signal: deadline.signal,
|
|
1709
|
-
timeoutMs: options.timeoutMs,
|
|
1710
|
-
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: void 0 } : void 0,
|
|
1711
|
-
sourceNodePath: options.sourceNodePath,
|
|
1712
|
-
concurrency: options.concurrency,
|
|
1713
|
-
retryOptions: options.retryOptions,
|
|
1714
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
1715
|
-
resumeChunks: options.resumeChunks,
|
|
1716
|
-
resumeChunkIndices: options.resumeChunkIndices,
|
|
1717
|
-
customMerger: options.customMerger,
|
|
1718
|
-
outputMimeType: options.outputMimeType,
|
|
1719
|
-
postMergeValidator: options.postMergeValidator,
|
|
1720
|
-
resumeValidation: options.resumeValidation,
|
|
1721
|
-
customHeaders: options.customHeaders,
|
|
1722
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1723
|
-
});
|
|
1724
|
-
return { ok: true, success: true, status: "success", value };
|
|
1725
|
-
}
|
|
1726
|
-
const inputs = chunks.map((chunk) => typeof chunk === "string" ? { ssml: chunk } : chunk);
|
|
1727
|
-
const fingerprints = inputs.map(
|
|
1728
|
-
(chunk) => computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
1729
|
-
customHeaders: options.customHeaders,
|
|
1730
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion
|
|
1731
|
-
})
|
|
1732
|
-
);
|
|
1733
|
-
const results = new Array(chunks.length);
|
|
1734
|
-
const chunkStates = inputs.map((_chunk, chunkIndex) => ({
|
|
1735
|
-
chunkIndex,
|
|
1736
|
-
status: "pending",
|
|
1737
|
-
canResume: true
|
|
1738
|
-
}));
|
|
1739
|
-
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1740
|
-
const invalidCachedIndices = /* @__PURE__ */ new Set();
|
|
1741
|
-
for (const [index, cached] of cachedChunks) {
|
|
1742
|
-
if (index < 0 || index >= chunks.length) continue;
|
|
1743
|
-
if (options.resumeValidation === "disabled" || cached.fingerprint === fingerprints[index]) {
|
|
1744
|
-
results[index] = cached;
|
|
1745
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: cached };
|
|
1746
|
-
} else invalidCachedIndices.add(index);
|
|
1747
|
-
}
|
|
1748
|
-
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1749
|
-
const shouldSynthesize = (index) => (!cachedChunks.has(index) || invalidCachedIndices.has(index)) && (requestedIndices === void 0 || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
1750
|
-
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
1751
|
-
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(deadline.signal, void 0) : void 0;
|
|
1752
|
-
fallbackJobScope = jobScope;
|
|
1753
|
-
const failedIndices = /* @__PURE__ */ new Set();
|
|
1754
|
-
let firstError;
|
|
1755
|
-
let completed = [...results].filter((result) => result !== void 0).length;
|
|
1756
|
-
let nextIndex = 0;
|
|
1757
|
-
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1758
|
-
const worker = async () => {
|
|
1759
|
-
while (true) {
|
|
1760
|
-
const index = nextIndex++;
|
|
1761
|
-
if (index >= chunks.length) return;
|
|
1762
|
-
if (!shouldSynthesize(index)) continue;
|
|
1763
|
-
if (firstError && options.cancelOnFailure !== false) {
|
|
1764
|
-
chunkStates[index] = { chunkIndex: index, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const chunk = chunks[index];
|
|
1768
|
-
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1769
|
-
const sourceNodePath = input.sourceNodePath;
|
|
1770
|
-
const originalTextRange = input.originalTextRange;
|
|
1771
|
-
pending(index, "synthesizing");
|
|
1772
|
-
const startedAt = Date.now();
|
|
1773
|
-
try {
|
|
1774
|
-
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1775
|
-
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1776
|
-
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
1777
|
-
let result;
|
|
1778
|
-
try {
|
|
1779
|
-
result = await retryableSynthesis(
|
|
1780
|
-
() => client.synthesizeSsml(input.ssml, {
|
|
1781
|
-
outputFormat: options.outputFormat,
|
|
1782
|
-
signal: chunkSignal,
|
|
1783
|
-
timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
|
|
1784
|
-
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1785
|
-
}),
|
|
1786
|
-
options.retryOptions,
|
|
1787
|
-
chunkSignal,
|
|
1788
|
-
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1789
|
-
currentChunk: completed,
|
|
1790
|
-
totalChunks: chunks.length,
|
|
1791
|
-
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1792
|
-
chunkIndex: index,
|
|
1793
|
-
originalTextRange: input.originalTextRange,
|
|
1794
|
-
status: "synthesizing",
|
|
1795
|
-
durationMs: Date.now() - startedAt,
|
|
1796
|
-
retryAttempt,
|
|
1797
|
-
nextRetryDelayMs,
|
|
1798
|
-
isRetrying: true
|
|
1799
|
-
}),
|
|
1800
|
-
jobDeadlineAt
|
|
1801
|
-
);
|
|
1802
|
-
} catch (error) {
|
|
1803
|
-
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
1804
|
-
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1805
|
-
throw error;
|
|
1806
|
-
} finally {
|
|
1807
|
-
chunkScope?.dispose();
|
|
1808
|
-
}
|
|
1809
|
-
results[index] = {
|
|
1810
|
-
...result,
|
|
1811
|
-
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1812
|
-
...sourceNodePath ? {
|
|
1813
|
-
boundaries: result.boundaries?.map((event) => ({
|
|
1814
|
-
...event,
|
|
1815
|
-
sourceNodePath: [...sourceNodePath],
|
|
1816
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1817
|
-
})),
|
|
1818
|
-
visemes: result.visemes?.map((event) => ({
|
|
1819
|
-
...event,
|
|
1820
|
-
sourceNodePath: [...sourceNodePath],
|
|
1821
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1822
|
-
})),
|
|
1823
|
-
bookmarks: result.bookmarks?.map((event) => ({
|
|
1824
|
-
...event,
|
|
1825
|
-
sourceNodePath: [...sourceNodePath],
|
|
1826
|
-
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1827
|
-
}))
|
|
1828
|
-
} : {},
|
|
1829
|
-
...originalTextRange ? {
|
|
1830
|
-
boundaries: result.boundaries?.map((event) => ({
|
|
1831
|
-
...event,
|
|
1832
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1833
|
-
})),
|
|
1834
|
-
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1835
|
-
...event,
|
|
1836
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1837
|
-
})),
|
|
1838
|
-
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1839
|
-
...event,
|
|
1840
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1841
|
-
})),
|
|
1842
|
-
visemes: result.visemes?.map((event) => ({
|
|
1843
|
-
...event,
|
|
1844
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1845
|
-
})),
|
|
1846
|
-
bookmarks: result.bookmarks?.map((event) => ({
|
|
1847
|
-
...event,
|
|
1848
|
-
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1849
|
-
}))
|
|
1850
|
-
} : {}
|
|
1851
|
-
};
|
|
1852
|
-
chunkStates[index] = { chunkIndex: index, status: "succeeded", canResume: true, result: results[index] };
|
|
1853
|
-
completed += 1;
|
|
1854
|
-
options.onProgress?.({
|
|
1855
|
-
currentChunk: completed,
|
|
1856
|
-
totalChunks: chunks.length,
|
|
1857
|
-
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1858
|
-
chunkIndex: index,
|
|
1859
|
-
originalTextRange: input.originalTextRange,
|
|
1860
|
-
status: "success",
|
|
1861
|
-
durationMs: Date.now() - startedAt
|
|
1862
|
-
});
|
|
1863
|
-
} catch (error) {
|
|
1864
|
-
const wasCancelled = firstError !== void 0 || !deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut());
|
|
1865
|
-
firstError ?? (firstError = deadline.timedOut ? new Error("Speech synthesis timed out.") : error);
|
|
1866
|
-
if (!wasCancelled) failedIndices.add(index);
|
|
1867
|
-
chunkStates[index] = {
|
|
1868
|
-
chunkIndex: index,
|
|
1869
|
-
status: wasCancelled ? "cancelled" : "failed",
|
|
1870
|
-
isOriginalFailure: !wasCancelled,
|
|
1871
|
-
canResume: true,
|
|
1872
|
-
error: serializeChunkError(error, "synthesis", !wasCancelled)
|
|
1873
|
-
};
|
|
1874
|
-
options.onProgress?.({
|
|
1875
|
-
currentChunk: completed,
|
|
1876
|
-
totalChunks: chunks.length,
|
|
1877
|
-
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1878
|
-
chunkIndex: index,
|
|
1879
|
-
originalTextRange: input.originalTextRange,
|
|
1880
|
-
status: "failed",
|
|
1881
|
-
durationMs: Date.now() - startedAt,
|
|
1882
|
-
error
|
|
1883
|
-
});
|
|
1884
|
-
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
};
|
|
1889
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1890
|
-
if (firstError && options.cancelOnFailure !== false) {
|
|
1891
|
-
for (const [chunkIndex, state] of chunkStates.entries()) {
|
|
1892
|
-
if (state.status === "pending" && shouldSynthesize(chunkIndex)) {
|
|
1893
|
-
chunkStates[chunkIndex] = { chunkIndex, status: "cancelled", isOriginalFailure: false, canResume: true };
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
if (failedIndices.size > 0) {
|
|
1898
|
-
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1899
|
-
const synthesizedChunks = results.flatMap(
|
|
1900
|
-
(result, chunkIndex) => result ? [{ ...result, chunkIndex, fingerprint: fingerprints[chunkIndex] ?? "" }] : []
|
|
1901
|
-
);
|
|
1902
|
-
error.partialResult = {
|
|
1903
|
-
synthesizedChunks,
|
|
1904
|
-
completedChunks: synthesizedChunks,
|
|
1905
|
-
pendingChunkIndices: chunkStates.flatMap(
|
|
1906
|
-
(state) => state.status === "pending" || state.status === "cancelled" || state.status === "failed" ? [state.chunkIndex] : []
|
|
1907
|
-
),
|
|
1908
|
-
failedChunkIndices: [...failedIndices],
|
|
1909
|
-
cancelledChunkIndices: chunkStates.filter((state) => state.status === "cancelled").map((state) => state.chunkIndex),
|
|
1910
|
-
chunkStates,
|
|
1911
|
-
totalChunks: chunks.length
|
|
1912
|
-
};
|
|
1913
|
-
throw error;
|
|
1914
|
-
}
|
|
1915
|
-
const missingChunkIndices = Array.from(
|
|
1916
|
-
{ length: chunks.length },
|
|
1917
|
-
(_value, index) => results[index] === void 0 ? index : void 0
|
|
1918
|
-
).filter((index) => index !== void 0);
|
|
1919
|
-
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
1920
|
-
const orderedResults = results.filter((result) => result !== void 0);
|
|
1921
|
-
return {
|
|
1922
|
-
ok: true,
|
|
1923
|
-
success: true,
|
|
1924
|
-
status: "success",
|
|
1925
|
-
value: await mergeSynthesisResults(orderedResults, {
|
|
1926
|
-
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1927
|
-
signal: jobScope?.signal ?? deadline.signal,
|
|
1928
|
-
customMerger: options.customMerger,
|
|
1929
|
-
outputMimeType: options.outputMimeType,
|
|
1930
|
-
postMergeValidator: options.postMergeValidator
|
|
1931
|
-
})
|
|
1932
|
-
};
|
|
1933
|
-
} catch (error) {
|
|
1934
|
-
const synthesisError = toSynthesisError(error);
|
|
1935
|
-
return failure(synthesisError, partialResultFrom(error));
|
|
1936
|
-
} finally {
|
|
1937
|
-
fallbackJobScope?.dispose();
|
|
1938
|
-
deadline.dispose();
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
function withValidationSignal(options, signal) {
|
|
1942
|
-
if (!signal) return options;
|
|
1943
|
-
return {
|
|
1944
|
-
...options,
|
|
1945
|
-
urlValidatorSignal: signal,
|
|
1946
|
-
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
// src/client.ts
|
|
1951
|
-
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
1952
|
-
var _options;
|
|
1953
|
-
var AzureTtsClient = class {
|
|
1954
|
-
constructor(options) {
|
|
1955
|
-
__privateAdd(this, _options);
|
|
1956
|
-
__privateSet(this, _options, options);
|
|
1957
|
-
}
|
|
1958
|
-
async synthesize(ssml) {
|
|
1959
|
-
const {
|
|
1960
|
-
region,
|
|
1961
|
-
subscriptionKey,
|
|
1962
|
-
outputFormat,
|
|
1963
|
-
signal,
|
|
1964
|
-
timeoutMs,
|
|
1965
|
-
timeouts,
|
|
1966
|
-
customHeaders,
|
|
1967
|
-
fingerprintSchemaVersion
|
|
1968
|
-
} = __privateGet(this, _options);
|
|
1969
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1970
|
-
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1971
|
-
const config = {
|
|
1972
|
-
endpoint,
|
|
1973
|
-
region,
|
|
1974
|
-
subscriptionKey,
|
|
1975
|
-
outputFormat,
|
|
1976
|
-
signal,
|
|
1977
|
-
timeoutMs,
|
|
1978
|
-
timeouts,
|
|
1979
|
-
retryOptions: __privateGet(this, _options).retryOptions,
|
|
1980
|
-
customHeaders,
|
|
1981
|
-
fingerprintSchemaVersion
|
|
1982
|
-
};
|
|
1983
|
-
return synthesizeSpeech(ssml, config);
|
|
1984
|
-
}
|
|
1985
|
-
async synthesizeSsml(ssml, options = {}) {
|
|
1986
|
-
const {
|
|
1987
|
-
region,
|
|
1988
|
-
subscriptionKey,
|
|
1989
|
-
outputFormat,
|
|
1990
|
-
signal,
|
|
1991
|
-
timeoutMs,
|
|
1992
|
-
timeouts,
|
|
1993
|
-
customHeaders,
|
|
1994
|
-
fingerprintSchemaVersion
|
|
1995
|
-
} = __privateGet(this, _options);
|
|
1996
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
1997
|
-
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
1998
|
-
return synthesizeSsml(ssml, {
|
|
1999
|
-
endpoint,
|
|
2000
|
-
region,
|
|
2001
|
-
subscriptionKey,
|
|
2002
|
-
outputFormat: options.outputFormat ?? outputFormat,
|
|
2003
|
-
signal: options.signal ?? signal,
|
|
2004
|
-
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
2005
|
-
timeouts: options.timeouts ?? timeouts,
|
|
2006
|
-
sourceNodePath: options.sourceNodePath,
|
|
2007
|
-
sourceTextSegments: options.sourceTextSegments,
|
|
2008
|
-
sourceMarkers: options.sourceMarkers,
|
|
2009
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2010
|
-
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2011
|
-
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2012
|
-
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2013
|
-
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2014
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2015
|
-
customHeaders: options.customHeaders ?? customHeaders,
|
|
2016
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
2017
|
-
});
|
|
2018
|
-
}
|
|
2019
|
-
async synthesizeChunks(chunks, options = {}) {
|
|
2020
|
-
const {
|
|
2021
|
-
region,
|
|
2022
|
-
subscriptionKey,
|
|
2023
|
-
outputFormat,
|
|
2024
|
-
signal,
|
|
2025
|
-
timeoutMs,
|
|
2026
|
-
timeouts,
|
|
2027
|
-
customHeaders,
|
|
2028
|
-
fingerprintSchemaVersion
|
|
2029
|
-
} = __privateGet(this, _options);
|
|
2030
|
-
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
2031
|
-
return synthesizeSsmlChunks(chunks, {
|
|
2032
|
-
endpoint,
|
|
2033
|
-
region,
|
|
2034
|
-
subscriptionKey,
|
|
2035
|
-
outputFormat: options.outputFormat ?? outputFormat,
|
|
2036
|
-
signal: options.signal ?? signal,
|
|
2037
|
-
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
2038
|
-
timeouts: options.timeouts ?? timeouts,
|
|
2039
|
-
sourceNodePath: options.sourceNodePath,
|
|
2040
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
2041
|
-
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
2042
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2043
|
-
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2044
|
-
resumeChunks: options.resumeChunks,
|
|
2045
|
-
resumeChunkIndices: options.resumeChunkIndices,
|
|
2046
|
-
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2047
|
-
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2048
|
-
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2049
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2050
|
-
customHeaders: options.customHeaders ?? customHeaders,
|
|
2051
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
2055
|
-
return synthesizeSsmlSafe(this, ssml, options);
|
|
2056
|
-
}
|
|
2057
|
-
async synthesizeChunksSafe(chunks, options = {}) {
|
|
2058
|
-
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
2059
|
-
...options,
|
|
2060
|
-
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
2061
|
-
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
2062
|
-
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
2063
|
-
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
2064
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
2065
|
-
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
2066
|
-
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
2067
|
-
cancelOnFailure: options.cancelOnFailure ?? __privateGet(this, _options).cancelOnFailure,
|
|
2068
|
-
customMerger: options.customMerger ?? __privateGet(this, _options).customMerger,
|
|
2069
|
-
outputMimeType: options.outputMimeType ?? __privateGet(this, _options).outputMimeType,
|
|
2070
|
-
postMergeValidator: options.postMergeValidator ?? __privateGet(this, _options).postMergeValidator,
|
|
2071
|
-
resumeValidation: options.resumeValidation ?? __privateGet(this, _options).resumeValidation,
|
|
2072
|
-
customHeaders: options.customHeaders ?? __privateGet(this, _options).customHeaders,
|
|
2073
|
-
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? __privateGet(this, _options).fingerprintSchemaVersion
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
2077
|
-
return this.synthesizeChunksSafe(chunks, options);
|
|
2078
|
-
}
|
|
2079
|
-
};
|
|
2080
|
-
_options = new WeakMap();
|
|
2081
|
-
|
|
2082
|
-
// src/voiceCatalog.ts
|
|
2083
|
-
var AZURE_VOICE_API_VERSION = "2025-10-01";
|
|
2084
|
-
function stringValue(value) {
|
|
2085
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2086
|
-
}
|
|
2087
|
-
function stringList(value) {
|
|
2088
|
-
if (!Array.isArray(value)) return [];
|
|
2089
|
-
return [...new Set(value.map(stringValue).filter((item) => item !== void 0))];
|
|
2090
|
-
}
|
|
2091
|
-
function normalizeStatus(value) {
|
|
2092
|
-
const status = stringValue(value)?.toLowerCase();
|
|
2093
|
-
if (status === "preview" || status === "deprecated" || status === "ga") return status;
|
|
2094
|
-
return void 0;
|
|
2095
|
-
}
|
|
2096
|
-
function normalizeRegions(region) {
|
|
2097
|
-
const regions = Array.isArray(region) ? region : [region];
|
|
2098
|
-
const result = [...new Set(regions.map((item) => item.trim()).filter(Boolean))];
|
|
2099
|
-
if (result.length === 0) throw new TypeError("At least one Azure Speech region is required.");
|
|
2100
|
-
return result;
|
|
2101
|
-
}
|
|
2102
|
-
async function fetchRegionVoices(region, apiKey) {
|
|
2103
|
-
const endpoint = `https://${encodeURIComponent(region)}.tts.speech.microsoft.com/cognitiveservices/voices/list`;
|
|
2104
|
-
const response = await fetch(endpoint, {
|
|
2105
|
-
headers: {
|
|
2106
|
-
Accept: "application/json",
|
|
2107
|
-
"Ocp-Apim-Subscription-Key": apiKey
|
|
2108
|
-
}
|
|
2109
|
-
});
|
|
2110
|
-
if (!response.ok) {
|
|
2111
|
-
throw new Error(`Azure List Voices API request failed for region "${region}" with HTTP ${response.status}.`);
|
|
2112
|
-
}
|
|
2113
|
-
const payload = await response.json();
|
|
2114
|
-
if (!Array.isArray(payload)) throw new Error(`Azure List Voices API returned an invalid response for "${region}".`);
|
|
2115
|
-
return payload.filter((item) => Boolean(item && typeof item === "object"));
|
|
2116
|
-
}
|
|
2117
|
-
async function fetchAzureVoiceCatalog(options) {
|
|
2118
|
-
if (!options || typeof options.apiKey !== "string" || !options.apiKey.trim())
|
|
2119
|
-
throw new TypeError("An Azure Speech API key is required.");
|
|
2120
|
-
const regions = normalizeRegions(options.region);
|
|
2121
|
-
const payloads = await Promise.all(regions.map((region) => fetchRegionVoices(region, options.apiKey)));
|
|
2122
|
-
const voices = /* @__PURE__ */ new Map();
|
|
2123
|
-
for (let regionIndex = 0; regionIndex < payloads.length; regionIndex += 1) {
|
|
2124
|
-
const region = regions[regionIndex];
|
|
2125
|
-
for (const record of payloads[regionIndex]) {
|
|
2126
|
-
const name = stringValue(record.ShortName) ?? stringValue(record.Name);
|
|
2127
|
-
const locale = stringValue(record.Locale);
|
|
2128
|
-
if (!name || !locale) continue;
|
|
2129
|
-
const key = name.toLowerCase();
|
|
2130
|
-
const existing = voices.get(key);
|
|
2131
|
-
const secondaryLocales = stringList(record.SecondaryLocaleList);
|
|
2132
|
-
const styles = stringList(record.StyleList);
|
|
2133
|
-
const status = normalizeStatus(record.Status);
|
|
2134
|
-
const supportedTags = stringList(record.SupportedTags);
|
|
2135
|
-
const unsupportedTags = stringList(record.UnsupportedTags);
|
|
2136
|
-
const models = stringList(record.Models);
|
|
2137
|
-
const merged = {
|
|
2138
|
-
name: existing?.name ?? name,
|
|
2139
|
-
locale: existing?.locale ?? locale,
|
|
2140
|
-
regions: [.../* @__PURE__ */ new Set([...existing?.regions ?? [], region])]
|
|
2141
|
-
};
|
|
2142
|
-
const mergedSecondaryLocales = [.../* @__PURE__ */ new Set([...existing?.secondaryLocales ?? [], ...secondaryLocales])];
|
|
2143
|
-
if (mergedSecondaryLocales.length > 0) merged.secondaryLocales = mergedSecondaryLocales;
|
|
2144
|
-
const mergedStyles = [.../* @__PURE__ */ new Set([...existing?.styles ?? [], ...styles])];
|
|
2145
|
-
if (mergedStyles.length > 0) merged.styles = mergedStyles;
|
|
2146
|
-
const mergedSupportedTags = [.../* @__PURE__ */ new Set([...existing?.supportedTags ?? [], ...supportedTags])];
|
|
2147
|
-
if (mergedSupportedTags.length > 0) merged.supportedTags = mergedSupportedTags;
|
|
2148
|
-
const mergedUnsupportedTags = [.../* @__PURE__ */ new Set([...existing?.unsupportedTags ?? [], ...unsupportedTags])];
|
|
2149
|
-
if (mergedUnsupportedTags.length > 0) merged.unsupportedTags = mergedUnsupportedTags;
|
|
2150
|
-
const mergedModels = [.../* @__PURE__ */ new Set([...existing?.models ?? [], ...models])];
|
|
2151
|
-
if (mergedModels.length > 0) merged.models = mergedModels;
|
|
2152
|
-
if (status) merged.status = status;
|
|
2153
|
-
else if (existing?.status) merged.status = existing.status;
|
|
2154
|
-
voices.set(key, merged);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
const sortedVoices = [...voices.values()].sort((first, second) => first.name.localeCompare(second.name));
|
|
2158
|
-
return {
|
|
2159
|
-
voices: sortedVoices,
|
|
2160
|
-
metadata: {
|
|
2161
|
-
voiceCount: sortedVoices.length,
|
|
2162
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2163
|
-
apiVersion: AZURE_VOICE_API_VERSION,
|
|
2164
|
-
regions,
|
|
2165
|
-
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
2166
|
-
regionDiffs: {}
|
|
2167
|
-
}
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
export {
|
|
2171
|
-
AudioFormatMismatchError,
|
|
2172
|
-
AzureTtsClient,
|
|
2173
|
-
AzureTtsError,
|
|
2174
|
-
AzureTtsSdkError,
|
|
2175
|
-
BatchChunkValidationError,
|
|
2176
|
-
ChunkValidationError,
|
|
2177
|
-
DEFAULT_OUTPUT_FORMAT,
|
|
2178
|
-
DeadlineController,
|
|
2179
|
-
IncompleteChunkSetError,
|
|
2180
|
-
MergeError,
|
|
2181
|
-
SynthesisCancelledError,
|
|
2182
|
-
SynthesisTimeoutError,
|
|
2183
|
-
UnsupportedMergeFormatError,
|
|
2184
|
-
canMergeAudioFormat,
|
|
2185
|
-
computeChunkFingerprint,
|
|
2186
|
-
fetchAzureVoiceCatalog,
|
|
2187
|
-
getRetryAfterDelayMs,
|
|
2188
|
-
inspectAudioSpecification,
|
|
2189
|
-
mergeAudioBuffers,
|
|
2190
|
-
mergeSynthesisResults,
|
|
2191
|
-
resolveMergeAudioFormat,
|
|
2192
|
-
resolveMimeType,
|
|
2193
|
-
serializeChunkError,
|
|
2194
|
-
synthesizeSpeech,
|
|
2195
|
-
synthesizeSsml,
|
|
2196
|
-
synthesizeSsmlChunks,
|
|
2197
|
-
synthesizeSsmlChunksSafe,
|
|
2198
|
-
synthesizeSsmlSafe
|
|
2199
|
-
};
|
|
2200
|
-
//# sourceMappingURL=index.mjs.map
|