@happyvertical/speech 0.80.0 → 0.80.2
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/dist/index.js +450 -627
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,694 +1,517 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
1
|
+
//#region src/shared/errors.ts
|
|
2
|
+
var SpeechError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
adapter;
|
|
5
|
+
constructor(message, code, adapter) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.adapter = adapter;
|
|
9
|
+
this.name = "SpeechError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var SpeechConfigurationError = class extends SpeechError {
|
|
13
|
+
constructor(message, adapter) {
|
|
14
|
+
super(message, "SPEECH_CONFIGURATION_ERROR", adapter);
|
|
15
|
+
this.name = "SpeechConfigurationError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var InvalidSpeechAdapterError = class extends SpeechError {
|
|
19
|
+
constructor(type, kind) {
|
|
20
|
+
super(`Invalid ${kind} speech adapter type: ${type}`, "INVALID_ADAPTER");
|
|
21
|
+
this.name = "InvalidSpeechAdapterError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var SpeechProviderError = class extends SpeechError {
|
|
25
|
+
status;
|
|
26
|
+
responseBody;
|
|
27
|
+
constructor(adapter, message, options = {}) {
|
|
28
|
+
super(message, "SPEECH_PROVIDER_ERROR", adapter);
|
|
29
|
+
this.name = "SpeechProviderError";
|
|
30
|
+
this.status = options.status;
|
|
31
|
+
this.responseBody = options.responseBody;
|
|
32
|
+
if (options.cause !== void 0) this.cause = options.cause;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/shared/http.ts
|
|
34
37
|
function normalizeBaseUrl(baseUrl) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
38
|
+
if (!baseUrl.trim()) throw new SpeechConfigurationError("Speech adapter baseUrl is required");
|
|
39
|
+
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
39
40
|
}
|
|
40
41
|
function resolveSpeechUrl(baseUrl, path) {
|
|
41
|
-
|
|
42
|
+
return new URL(path.replace(/^\//, ""), normalizeBaseUrl(baseUrl)).toString();
|
|
42
43
|
}
|
|
43
44
|
function resolveFetch(fetchOverride) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!globalFetch) {
|
|
49
|
-
throw new SpeechConfigurationError(
|
|
50
|
-
"No fetch implementation available for speech adapter"
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
return globalFetch.bind(globalThis);
|
|
45
|
+
if (fetchOverride) return fetchOverride;
|
|
46
|
+
const globalFetch = globalThis.fetch;
|
|
47
|
+
if (!globalFetch) throw new SpeechConfigurationError("No fetch implementation available for speech adapter");
|
|
48
|
+
return globalFetch.bind(globalThis);
|
|
54
49
|
}
|
|
55
50
|
function mergeHeaders(options, extra) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
await assertOk(response, adapterName);
|
|
101
|
-
return await readResponse(response);
|
|
102
|
-
} finally {
|
|
103
|
-
if (timeout) {
|
|
104
|
-
clearTimeout(timeout);
|
|
105
|
-
}
|
|
106
|
-
requestSignal.cleanup();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
51
|
+
const headers = new Headers(options.headers);
|
|
52
|
+
if (options.apiKey && !headers.has("authorization")) headers.set("authorization", `Bearer ${options.apiKey}`);
|
|
53
|
+
if (extra) new Headers(extra).forEach((value, key) => {
|
|
54
|
+
headers.set(key, value);
|
|
55
|
+
});
|
|
56
|
+
return headers;
|
|
57
|
+
}
|
|
58
|
+
var HttpSpeechAdapter = class {
|
|
59
|
+
baseUrl;
|
|
60
|
+
fetchImpl;
|
|
61
|
+
apiKey;
|
|
62
|
+
headers;
|
|
63
|
+
timeoutMs;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
this.baseUrl = options.baseUrl;
|
|
66
|
+
this.fetchImpl = resolveFetch(options.fetch);
|
|
67
|
+
this.apiKey = options.apiKey;
|
|
68
|
+
this.headers = options.headers;
|
|
69
|
+
this.timeoutMs = options.timeoutMs;
|
|
70
|
+
}
|
|
71
|
+
async post(adapterName, path, init, readResponse) {
|
|
72
|
+
const timeoutController = this.timeoutMs && this.timeoutMs > 0 ? new AbortController() : void 0;
|
|
73
|
+
const timeout = timeoutController && this.timeoutMs ? setTimeout(() => timeoutController.abort(), this.timeoutMs) : void 0;
|
|
74
|
+
const requestSignal = composeAbortSignals(init.signal, timeoutController?.signal);
|
|
75
|
+
try {
|
|
76
|
+
const response = await this.fetchImpl(resolveSpeechUrl(this.baseUrl, path), {
|
|
77
|
+
...init,
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: mergeHeaders({
|
|
80
|
+
apiKey: this.apiKey,
|
|
81
|
+
headers: this.headers
|
|
82
|
+
}, init.headers),
|
|
83
|
+
signal: requestSignal.signal
|
|
84
|
+
});
|
|
85
|
+
await assertOk(response, adapterName);
|
|
86
|
+
return await readResponse(response);
|
|
87
|
+
} finally {
|
|
88
|
+
if (timeout) clearTimeout(timeout);
|
|
89
|
+
requestSignal.cleanup();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
110
93
|
function composeAbortSignals(...signals) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
for (const signal of activeSignals) {
|
|
143
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
144
|
-
}
|
|
145
|
-
return { signal: controller.signal, cleanup };
|
|
94
|
+
const activeSignals = signals.filter((signal) => Boolean(signal));
|
|
95
|
+
if (activeSignals.length === 0) return { cleanup: () => void 0 };
|
|
96
|
+
if (activeSignals.length === 1) return {
|
|
97
|
+
signal: activeSignals[0],
|
|
98
|
+
cleanup: () => void 0
|
|
99
|
+
};
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
let cleanup = () => void 0;
|
|
102
|
+
const abortFrom = (signal) => {
|
|
103
|
+
if (!controller.signal.aborted) controller.abort(signal.reason);
|
|
104
|
+
cleanup();
|
|
105
|
+
};
|
|
106
|
+
const onAbort = (event) => {
|
|
107
|
+
abortFrom(event.target);
|
|
108
|
+
};
|
|
109
|
+
cleanup = () => {
|
|
110
|
+
for (const signal of activeSignals) signal.removeEventListener("abort", onAbort);
|
|
111
|
+
};
|
|
112
|
+
for (const signal of activeSignals) if (signal.aborted) {
|
|
113
|
+
abortFrom(signal);
|
|
114
|
+
return {
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
cleanup: () => void 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
for (const signal of activeSignals) signal.addEventListener("abort", onAbort, { once: true });
|
|
120
|
+
return {
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
cleanup
|
|
123
|
+
};
|
|
146
124
|
}
|
|
147
125
|
async function assertOk(response, adapterName) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`${adapterName} speech request failed with HTTP ${response.status}`,
|
|
155
|
-
{
|
|
156
|
-
status: response.status,
|
|
157
|
-
responseBody
|
|
158
|
-
}
|
|
159
|
-
);
|
|
126
|
+
if (response.ok) return;
|
|
127
|
+
const responseBody = await response.text().catch(() => void 0);
|
|
128
|
+
throw new SpeechProviderError(adapterName, `${adapterName} speech request failed with HTTP ${response.status}`, {
|
|
129
|
+
status: response.status,
|
|
130
|
+
responseBody
|
|
131
|
+
});
|
|
160
132
|
}
|
|
161
133
|
function appendAudioInput(form, audio, fieldName = "audio") {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
134
|
+
const filename = audio.filename ?? "audio";
|
|
135
|
+
const data = audio.data;
|
|
136
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
137
|
+
const contentType = audio.contentType ?? (data.type || "application/octet-stream");
|
|
138
|
+
const blob = data.type === contentType ? data : new Blob([data], { type: contentType });
|
|
139
|
+
form.append(fieldName, blob, filename);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const contentType = audio.contentType ?? "application/octet-stream";
|
|
143
|
+
const blobPart = data instanceof Uint8Array ? arrayBufferFromBytes(data) : data;
|
|
144
|
+
form.append(fieldName, new Blob([blobPart], { type: contentType }), filename);
|
|
173
145
|
}
|
|
174
146
|
function appendOptionalFormValue(form, name, value) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
form.append(name, String(value));
|
|
147
|
+
if (value === void 0 || value === null) return;
|
|
148
|
+
form.append(name, String(value));
|
|
179
149
|
}
|
|
180
150
|
function createHappyVerticalSynthesisForm(request, defaultVoice) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
appendOptionalFormValue(
|
|
190
|
-
form,
|
|
191
|
-
"speaker",
|
|
192
|
-
voiceToString(request.voice, defaultVoice)
|
|
193
|
-
);
|
|
194
|
-
appendOptionalFormValue(form, "speed", request.speed);
|
|
195
|
-
appendOptionalFormValue(form, "voice_prompt", voice?.prompt);
|
|
196
|
-
return form;
|
|
151
|
+
const form = new FormData();
|
|
152
|
+
const voice = request.voice && typeof request.voice !== "string" ? request.voice : void 0;
|
|
153
|
+
form.set("text", request.text);
|
|
154
|
+
appendOptionalFormValue(form, "language", request.language ?? voice?.language);
|
|
155
|
+
appendOptionalFormValue(form, "speaker", voiceToString(request.voice, defaultVoice));
|
|
156
|
+
appendOptionalFormValue(form, "speed", request.speed);
|
|
157
|
+
appendOptionalFormValue(form, "voice_prompt", voice?.prompt);
|
|
158
|
+
return form;
|
|
197
159
|
}
|
|
198
160
|
function voiceToString(voice, fallback) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (typeof voice === "string") {
|
|
203
|
-
return voice;
|
|
204
|
-
}
|
|
205
|
-
return voice.id ?? voice.name ?? voice.speakerId ?? fallback;
|
|
161
|
+
if (!voice) return fallback;
|
|
162
|
+
if (typeof voice === "string") return voice;
|
|
163
|
+
return voice.id ?? voice.name ?? voice.speakerId ?? fallback;
|
|
206
164
|
}
|
|
207
165
|
function compactJson(value) {
|
|
208
|
-
|
|
209
|
-
Object.entries(value).filter(([, entryValue]) => entryValue !== void 0)
|
|
210
|
-
);
|
|
166
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== void 0));
|
|
211
167
|
}
|
|
212
168
|
function contentTypeToFormat(contentType) {
|
|
213
|
-
|
|
214
|
-
return match?.[1];
|
|
169
|
+
return /^audio\/([^;]+)/.exec(contentType)?.[1];
|
|
215
170
|
}
|
|
216
171
|
function arrayBufferFromBytes(bytes) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
172
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
173
|
+
copy.set(bytes);
|
|
174
|
+
return copy.buffer;
|
|
220
175
|
}
|
|
221
176
|
function decodeBase64(base64) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
228
|
-
bytes[index] = binary.charCodeAt(index);
|
|
229
|
-
}
|
|
230
|
-
return bytes.buffer;
|
|
177
|
+
if (typeof Buffer !== "undefined") return arrayBufferFromBytes(Buffer.from(base64, "base64"));
|
|
178
|
+
const binary = atob(base64);
|
|
179
|
+
const bytes = new Uint8Array(binary.length);
|
|
180
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
181
|
+
return bytes.buffer;
|
|
231
182
|
}
|
|
232
183
|
function getNumber(value, ...keys) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return void 0;
|
|
184
|
+
for (const key of keys) {
|
|
185
|
+
const candidate = value[key];
|
|
186
|
+
if (typeof candidate === "number") return candidate;
|
|
187
|
+
}
|
|
240
188
|
}
|
|
241
189
|
function getString(value, ...keys) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return void 0;
|
|
190
|
+
for (const key of keys) {
|
|
191
|
+
const candidate = value[key];
|
|
192
|
+
if (typeof candidate === "string") return candidate;
|
|
193
|
+
}
|
|
249
194
|
}
|
|
250
195
|
function normalizeWordTimings(value) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!word || startSeconds === void 0 || endSeconds === void 0) {
|
|
268
|
-
return [];
|
|
269
|
-
}
|
|
270
|
-
return [
|
|
271
|
-
{
|
|
272
|
-
word,
|
|
273
|
-
startSeconds,
|
|
274
|
-
endSeconds,
|
|
275
|
-
confidence: getNumber(record, "confidence"),
|
|
276
|
-
speakerId: getString(record, "speakerId", "speaker", "speaker_id")
|
|
277
|
-
}
|
|
278
|
-
];
|
|
279
|
-
});
|
|
196
|
+
if (!Array.isArray(value)) return;
|
|
197
|
+
return value.flatMap((entry) => {
|
|
198
|
+
if (!entry || typeof entry !== "object") return [];
|
|
199
|
+
const record = entry;
|
|
200
|
+
const word = getString(record, "word", "text", "token");
|
|
201
|
+
const startSeconds = getNumber(record, "startSeconds", "start", "start_time");
|
|
202
|
+
const endSeconds = getNumber(record, "endSeconds", "end", "end_time");
|
|
203
|
+
if (!word || startSeconds === void 0 || endSeconds === void 0) return [];
|
|
204
|
+
return [{
|
|
205
|
+
word,
|
|
206
|
+
startSeconds,
|
|
207
|
+
endSeconds,
|
|
208
|
+
confidence: getNumber(record, "confidence"),
|
|
209
|
+
speakerId: getString(record, "speakerId", "speaker", "speaker_id")
|
|
210
|
+
}];
|
|
211
|
+
});
|
|
280
212
|
}
|
|
281
213
|
function normalizeTranscriptSegments(value) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
text,
|
|
297
|
-
startSeconds: getNumber(record, "startSeconds", "start", "start_time"),
|
|
298
|
-
endSeconds: getNumber(record, "endSeconds", "end", "end_time"),
|
|
299
|
-
confidence: getNumber(record, "confidence"),
|
|
300
|
-
speakerId: getString(record, "speakerId", "speaker", "speaker_id")
|
|
301
|
-
}
|
|
302
|
-
];
|
|
303
|
-
});
|
|
214
|
+
if (!Array.isArray(value)) return;
|
|
215
|
+
return value.flatMap((entry) => {
|
|
216
|
+
if (!entry || typeof entry !== "object") return [];
|
|
217
|
+
const record = entry;
|
|
218
|
+
const text = getString(record, "text", "transcript");
|
|
219
|
+
if (!text) return [];
|
|
220
|
+
return [{
|
|
221
|
+
text,
|
|
222
|
+
startSeconds: getNumber(record, "startSeconds", "start", "start_time"),
|
|
223
|
+
endSeconds: getNumber(record, "endSeconds", "end", "end_time"),
|
|
224
|
+
confidence: getNumber(record, "confidence"),
|
|
225
|
+
speakerId: getString(record, "speakerId", "speaker", "speaker_id")
|
|
226
|
+
}];
|
|
227
|
+
});
|
|
304
228
|
}
|
|
305
229
|
async function readTranscriptResponse(response, adapterName) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
json.words ?? json.wordTimings ?? json.word_timings
|
|
322
|
-
),
|
|
323
|
-
segments: normalizeTranscriptSegments(json.segments),
|
|
324
|
-
provider: getString(json, "provider") ?? adapterName,
|
|
325
|
-
model: getString(json, "model"),
|
|
326
|
-
raw: json
|
|
327
|
-
};
|
|
230
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return {
|
|
231
|
+
text: await response.text(),
|
|
232
|
+
provider: adapterName
|
|
233
|
+
};
|
|
234
|
+
const json = await response.json();
|
|
235
|
+
return {
|
|
236
|
+
text: getString(json, "text", "transcript", "transcription") ?? "",
|
|
237
|
+
language: getString(json, "language", "lang"),
|
|
238
|
+
durationSeconds: getNumber(json, "durationSeconds", "duration"),
|
|
239
|
+
words: normalizeWordTimings(json.words ?? json.wordTimings ?? json.word_timings),
|
|
240
|
+
segments: normalizeTranscriptSegments(json.segments),
|
|
241
|
+
provider: getString(json, "provider") ?? adapterName,
|
|
242
|
+
model: getString(json, "model"),
|
|
243
|
+
raw: json
|
|
244
|
+
};
|
|
328
245
|
}
|
|
329
246
|
async function readSynthesizedSpeechResponse(response, adapterName) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
contentType,
|
|
359
|
-
format: getString(json, "format", "response_format") ?? contentTypeToFormat(contentType),
|
|
360
|
-
sampleRate: getNumber(json, "sampleRate", "sample_rate"),
|
|
361
|
-
channels: getNumber(json, "channels"),
|
|
362
|
-
durationSeconds: getNumber(json, "durationSeconds", "duration"),
|
|
363
|
-
words: normalizeWordTimings(
|
|
364
|
-
json.words ?? json.wordTimings ?? json.word_timings
|
|
365
|
-
),
|
|
366
|
-
provider: getString(json, "provider") ?? adapterName,
|
|
367
|
-
model: getString(json, "model"),
|
|
368
|
-
raw: json
|
|
369
|
-
};
|
|
247
|
+
const responseContentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
248
|
+
if (!responseContentType.includes("application/json")) {
|
|
249
|
+
const audio = await response.arrayBuffer();
|
|
250
|
+
const sampleRate = parseHeaderNumber(response, "x-sample-rate");
|
|
251
|
+
return {
|
|
252
|
+
audio,
|
|
253
|
+
contentType: responseContentType,
|
|
254
|
+
format: contentTypeToFormat(responseContentType),
|
|
255
|
+
sampleRate,
|
|
256
|
+
provider: adapterName
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const json = await response.json();
|
|
260
|
+
const contentType = getString(json, "contentType", "content_type", "mimeType", "mime_type") ?? "application/octet-stream";
|
|
261
|
+
const encodedAudio = getString(json, "audio", "audioContent", "audio_content");
|
|
262
|
+
if (!encodedAudio) throw new SpeechProviderError(adapterName, `${adapterName} JSON speech response did not include base64 audio`);
|
|
263
|
+
return {
|
|
264
|
+
audio: decodeBase64(encodedAudio),
|
|
265
|
+
contentType,
|
|
266
|
+
format: getString(json, "format", "response_format") ?? contentTypeToFormat(contentType),
|
|
267
|
+
sampleRate: getNumber(json, "sampleRate", "sample_rate"),
|
|
268
|
+
channels: getNumber(json, "channels"),
|
|
269
|
+
durationSeconds: getNumber(json, "durationSeconds", "duration"),
|
|
270
|
+
words: normalizeWordTimings(json.words ?? json.wordTimings ?? json.word_timings),
|
|
271
|
+
provider: getString(json, "provider") ?? adapterName,
|
|
272
|
+
model: getString(json, "model"),
|
|
273
|
+
raw: json
|
|
274
|
+
};
|
|
370
275
|
}
|
|
371
276
|
function parseHeaderNumber(response, headerName) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.type,
|
|
483
|
-
this.synthesizePath,
|
|
484
|
-
{
|
|
485
|
-
body: form,
|
|
486
|
-
signal: request.signal
|
|
487
|
-
},
|
|
488
|
-
(response) => readSynthesizedSpeechResponse(response, this.type)
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
277
|
+
const value = response.headers.get(headerName);
|
|
278
|
+
if (!value) return;
|
|
279
|
+
const parsed = Number(value);
|
|
280
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/adapters/openai-compatible.ts
|
|
284
|
+
var OpenAICompatibleSpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
285
|
+
type = "openai-compatible";
|
|
286
|
+
speechPath;
|
|
287
|
+
defaultModel;
|
|
288
|
+
defaultVoice;
|
|
289
|
+
constructor(options) {
|
|
290
|
+
super(options);
|
|
291
|
+
this.speechPath = options.speechPath ?? "/v1/audio/speech";
|
|
292
|
+
this.defaultModel = options.defaultModel ?? "tts-1";
|
|
293
|
+
this.defaultVoice = options.defaultVoice ?? "alloy";
|
|
294
|
+
}
|
|
295
|
+
async synthesize(request) {
|
|
296
|
+
const outputFormat = request.outputFormat ?? "mp3";
|
|
297
|
+
const payload = compactJson({
|
|
298
|
+
model: request.model ?? this.defaultModel,
|
|
299
|
+
input: request.text,
|
|
300
|
+
voice: voiceToString(request.voice, this.defaultVoice),
|
|
301
|
+
response_format: outputFormat,
|
|
302
|
+
speed: request.speed
|
|
303
|
+
});
|
|
304
|
+
const speech = await this.post(this.type, this.speechPath, {
|
|
305
|
+
headers: { "content-type": "application/json" },
|
|
306
|
+
body: JSON.stringify(payload),
|
|
307
|
+
signal: request.signal
|
|
308
|
+
}, (response) => readSynthesizedSpeechResponse(response, this.type));
|
|
309
|
+
return {
|
|
310
|
+
...speech,
|
|
311
|
+
format: speech.format ?? outputFormat,
|
|
312
|
+
model: speech.model ?? String(payload.model)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/adapters/qwen3.ts
|
|
318
|
+
var Qwen3SpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
319
|
+
type = "qwen3-tts";
|
|
320
|
+
speechPath;
|
|
321
|
+
defaultModel;
|
|
322
|
+
defaultVoice;
|
|
323
|
+
constructor(options) {
|
|
324
|
+
super(options);
|
|
325
|
+
this.speechPath = options.speechPath ?? "/v1/audio/speech";
|
|
326
|
+
this.defaultModel = options.defaultModel ?? "qwen3-tts";
|
|
327
|
+
this.defaultVoice = options.defaultVoice;
|
|
328
|
+
}
|
|
329
|
+
async synthesize(request) {
|
|
330
|
+
const form = createHappyVerticalSynthesisForm(request, this.defaultVoice);
|
|
331
|
+
const speech = await this.post(this.type, this.speechPath, {
|
|
332
|
+
body: form,
|
|
333
|
+
signal: request.signal
|
|
334
|
+
}, (response) => readSynthesizedSpeechResponse(response, this.type));
|
|
335
|
+
return {
|
|
336
|
+
...speech,
|
|
337
|
+
format: speech.format ?? request.outputFormat,
|
|
338
|
+
model: speech.model ?? request.model ?? this.defaultModel
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/adapters/studio-server.ts
|
|
344
|
+
var StudioServerTranscriber = class extends HttpSpeechAdapter {
|
|
345
|
+
type = "studio-server";
|
|
346
|
+
transcribePath;
|
|
347
|
+
constructor(options) {
|
|
348
|
+
super(options);
|
|
349
|
+
this.transcribePath = options.transcribePath ?? "/v1/transcribe";
|
|
350
|
+
}
|
|
351
|
+
async transcribe(request) {
|
|
352
|
+
const form = new FormData();
|
|
353
|
+
appendAudioInput(form, request.audio);
|
|
354
|
+
appendOptionalFormValue(form, "language", request.language);
|
|
355
|
+
return this.post(this.type, this.transcribePath, {
|
|
356
|
+
body: form,
|
|
357
|
+
signal: request.signal
|
|
358
|
+
}, (response) => readTranscriptResponse(response, this.type));
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
var StudioServerSpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
362
|
+
type = "studio-server";
|
|
363
|
+
synthesizePath;
|
|
364
|
+
defaultVoice;
|
|
365
|
+
constructor(options) {
|
|
366
|
+
super(options);
|
|
367
|
+
this.synthesizePath = options.synthesizePath ?? "/v1/tts/synthesize";
|
|
368
|
+
this.defaultVoice = options.defaultVoice;
|
|
369
|
+
}
|
|
370
|
+
async synthesize(request) {
|
|
371
|
+
const form = createHappyVerticalSynthesisForm(request, this.defaultVoice);
|
|
372
|
+
return this.post(this.type, this.synthesizePath, {
|
|
373
|
+
body: form,
|
|
374
|
+
signal: request.signal
|
|
375
|
+
}, (response) => readSynthesizedSpeechResponse(response, this.type));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/shared/factory.ts
|
|
380
|
+
/**
|
|
381
|
+
* Creates a speech service from explicit options and/or environment variables.
|
|
382
|
+
*
|
|
383
|
+
* Explicit options win over environment defaults. If no STT or TTS config is
|
|
384
|
+
* available, the returned service is still usable for dependency injection, but
|
|
385
|
+
* calling the missing operation throws a configuration error.
|
|
386
|
+
*/
|
|
492
387
|
async function getSpeech(options = {}, context = {}) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
return synthesizer.synthesize(request);
|
|
509
|
-
}
|
|
510
|
-
};
|
|
388
|
+
const transcriber = options.transcriber === false ? void 0 : await getOptionalTranscriber(options.transcriber, context);
|
|
389
|
+
const synthesizer = options.synthesizer === false ? void 0 : await getOptionalSpeechSynthesizer(options.synthesizer, context);
|
|
390
|
+
return {
|
|
391
|
+
transcriber,
|
|
392
|
+
synthesizer,
|
|
393
|
+
async transcribe(request) {
|
|
394
|
+
if (!transcriber) throw new SpeechConfigurationError("No STT provider configured");
|
|
395
|
+
return transcriber.transcribe(request);
|
|
396
|
+
},
|
|
397
|
+
async synthesize(request) {
|
|
398
|
+
if (!synthesizer) throw new SpeechConfigurationError("No TTS provider configured");
|
|
399
|
+
return synthesizer.synthesize(request);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
511
402
|
}
|
|
512
403
|
async function getTranscriber(options = {}, context = {}) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
throw new InvalidSpeechAdapterError(
|
|
519
|
-
resolved.type ?? "unknown",
|
|
520
|
-
"STT"
|
|
521
|
-
);
|
|
522
|
-
}
|
|
404
|
+
const resolved = normalizeTranscriberOptions(options, context);
|
|
405
|
+
switch (resolved.type) {
|
|
406
|
+
case "studio-server": return new StudioServerTranscriber(resolved);
|
|
407
|
+
default: throw new InvalidSpeechAdapterError(resolved.type ?? "unknown", "STT");
|
|
408
|
+
}
|
|
523
409
|
}
|
|
524
410
|
async function getSpeechSynthesizer(options, context = {}) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return new OpenAICompatibleSpeechSynthesizer(resolved);
|
|
533
|
-
default:
|
|
534
|
-
throw new InvalidSpeechAdapterError(
|
|
535
|
-
resolved.type ?? "unknown",
|
|
536
|
-
"TTS"
|
|
537
|
-
);
|
|
538
|
-
}
|
|
411
|
+
const resolved = normalizeSpeechSynthesizerOptions(options, context);
|
|
412
|
+
switch (resolved.type) {
|
|
413
|
+
case "studio-server": return new StudioServerSpeechSynthesizer(resolved);
|
|
414
|
+
case "qwen3-tts": return new Qwen3SpeechSynthesizer(resolved);
|
|
415
|
+
case "openai-compatible": return new OpenAICompatibleSpeechSynthesizer(resolved);
|
|
416
|
+
default: throw new InvalidSpeechAdapterError(resolved.type ?? "unknown", "TTS");
|
|
417
|
+
}
|
|
539
418
|
}
|
|
540
419
|
function getAvailableSpeechAdapters() {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
420
|
+
return {
|
|
421
|
+
transcribers: ["studio-server"],
|
|
422
|
+
synthesizers: [
|
|
423
|
+
"studio-server",
|
|
424
|
+
"qwen3-tts",
|
|
425
|
+
"openai-compatible"
|
|
426
|
+
]
|
|
427
|
+
};
|
|
545
428
|
}
|
|
546
429
|
async function getOptionalTranscriber(options, context) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
return getTranscriber(options, context);
|
|
430
|
+
if (!options && !hasTranscriberEnv(context.env ?? defaultEnv())) return;
|
|
431
|
+
return getTranscriber(options, context);
|
|
551
432
|
}
|
|
552
433
|
async function getOptionalSpeechSynthesizer(options, context) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
return getSpeechSynthesizer(options, context);
|
|
434
|
+
if (!options && !hasSynthesizerEnv(context.env ?? defaultEnv())) return;
|
|
435
|
+
return getSpeechSynthesizer(options, context);
|
|
557
436
|
}
|
|
558
437
|
function normalizeTranscriberOptions(options = {}, context) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
...options,
|
|
575
|
-
type,
|
|
576
|
-
baseUrl: baseUrl.trim(),
|
|
577
|
-
fetch: options.fetch ?? context.fetch,
|
|
578
|
-
headers: options.headers ?? context.headers,
|
|
579
|
-
apiKey: options.apiKey ?? readEnv(env, "HAVE_SPEECH_STT_API_KEY", "STT_API_KEY", "SPEECH_API_KEY"),
|
|
580
|
-
timeoutMs: options.timeoutMs ?? parseOptionalInteger(
|
|
581
|
-
readEnv(env, "HAVE_SPEECH_STT_TIMEOUT_MS", "STT_TIMEOUT_MS")
|
|
582
|
-
),
|
|
583
|
-
transcribePath: options.transcribePath ?? readEnv(env, "HAVE_SPEECH_STT_PATH", "STT_PATH")
|
|
584
|
-
};
|
|
438
|
+
const env = context.env ?? defaultEnv();
|
|
439
|
+
const type = options.type ?? readEnv(env, "HAVE_SPEECH_STT_TYPE", "HAVE_SPEECH_STT_ADAPTER", "STT_ADAPTER") ?? "studio-server";
|
|
440
|
+
const baseUrl = options.baseUrl ?? readEnv(env, "HAVE_SPEECH_STT_BASE_URL", "STT_BASE_URL");
|
|
441
|
+
if (type !== "studio-server") throw new InvalidSpeechAdapterError(type, "STT");
|
|
442
|
+
if (!baseUrl?.trim()) throw new SpeechConfigurationError("STT baseUrl is required", type);
|
|
443
|
+
return {
|
|
444
|
+
...options,
|
|
445
|
+
type,
|
|
446
|
+
baseUrl: baseUrl.trim(),
|
|
447
|
+
fetch: options.fetch ?? context.fetch,
|
|
448
|
+
headers: options.headers ?? context.headers,
|
|
449
|
+
apiKey: options.apiKey ?? readEnv(env, "HAVE_SPEECH_STT_API_KEY", "STT_API_KEY", "SPEECH_API_KEY"),
|
|
450
|
+
timeoutMs: options.timeoutMs ?? parseOptionalInteger(readEnv(env, "HAVE_SPEECH_STT_TIMEOUT_MS", "STT_TIMEOUT_MS")),
|
|
451
|
+
transcribePath: options.transcribePath ?? readEnv(env, "HAVE_SPEECH_STT_PATH", "STT_PATH")
|
|
452
|
+
};
|
|
585
453
|
}
|
|
586
454
|
function normalizeSpeechSynthesizerOptions(options, context) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (type === "qwen3-tts") {
|
|
623
|
-
return {
|
|
624
|
-
...shared,
|
|
625
|
-
type,
|
|
626
|
-
speechPath: options?.speechPath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
|
|
627
|
-
defaultModel: options?.defaultModel ?? readEnv(env, "HAVE_SPEECH_TTS_MODEL", "TTS_MODEL"),
|
|
628
|
-
defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
if (type === "openai-compatible") {
|
|
632
|
-
return {
|
|
633
|
-
...shared,
|
|
634
|
-
type,
|
|
635
|
-
speechPath: options?.speechPath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
|
|
636
|
-
defaultModel: options?.defaultModel ?? readEnv(env, "HAVE_SPEECH_TTS_MODEL", "TTS_MODEL"),
|
|
637
|
-
defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
throw new InvalidSpeechAdapterError(type, "TTS");
|
|
455
|
+
const env = context.env ?? defaultEnv();
|
|
456
|
+
const type = options?.type ?? readEnv(env, "HAVE_SPEECH_TTS_TYPE", "HAVE_SPEECH_TTS_ADAPTER", "TTS_ADAPTER");
|
|
457
|
+
const baseUrl = options?.baseUrl ?? readEnv(env, "HAVE_SPEECH_TTS_BASE_URL", "TTS_BASE_URL");
|
|
458
|
+
if (!type) throw new SpeechConfigurationError("TTS provider type is required");
|
|
459
|
+
if (type !== "studio-server" && type !== "qwen3-tts" && type !== "openai-compatible") throw new InvalidSpeechAdapterError(type, "TTS");
|
|
460
|
+
if (!baseUrl?.trim()) throw new SpeechConfigurationError("TTS baseUrl is required", type);
|
|
461
|
+
const shared = {
|
|
462
|
+
type,
|
|
463
|
+
baseUrl: baseUrl.trim(),
|
|
464
|
+
fetch: options?.fetch ?? context.fetch,
|
|
465
|
+
headers: options?.headers ?? context.headers,
|
|
466
|
+
apiKey: options?.apiKey ?? readEnv(env, "HAVE_SPEECH_TTS_API_KEY", "TTS_API_KEY", "SPEECH_API_KEY"),
|
|
467
|
+
timeoutMs: options?.timeoutMs ?? parseOptionalInteger(readEnv(env, "HAVE_SPEECH_TTS_TIMEOUT_MS", "TTS_TIMEOUT_MS"))
|
|
468
|
+
};
|
|
469
|
+
if (type === "studio-server") return {
|
|
470
|
+
...shared,
|
|
471
|
+
type,
|
|
472
|
+
synthesizePath: options?.synthesizePath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
|
|
473
|
+
defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
|
|
474
|
+
};
|
|
475
|
+
if (type === "qwen3-tts") return {
|
|
476
|
+
...shared,
|
|
477
|
+
type,
|
|
478
|
+
speechPath: options?.speechPath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
|
|
479
|
+
defaultModel: options?.defaultModel ?? readEnv(env, "HAVE_SPEECH_TTS_MODEL", "TTS_MODEL"),
|
|
480
|
+
defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
|
|
481
|
+
};
|
|
482
|
+
if (type === "openai-compatible") return {
|
|
483
|
+
...shared,
|
|
484
|
+
type,
|
|
485
|
+
speechPath: options?.speechPath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
|
|
486
|
+
defaultModel: options?.defaultModel ?? readEnv(env, "HAVE_SPEECH_TTS_MODEL", "TTS_MODEL"),
|
|
487
|
+
defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
|
|
488
|
+
};
|
|
489
|
+
throw new InvalidSpeechAdapterError(type, "TTS");
|
|
641
490
|
}
|
|
642
491
|
function hasTranscriberEnv(env) {
|
|
643
|
-
|
|
644
|
-
env,
|
|
645
|
-
"HAVE_SPEECH_STT_TYPE",
|
|
646
|
-
"HAVE_SPEECH_STT_ADAPTER",
|
|
647
|
-
"HAVE_SPEECH_STT_BASE_URL",
|
|
648
|
-
"STT_ADAPTER",
|
|
649
|
-
"STT_BASE_URL"
|
|
650
|
-
);
|
|
492
|
+
return hasAnyEnv(env, "HAVE_SPEECH_STT_TYPE", "HAVE_SPEECH_STT_ADAPTER", "HAVE_SPEECH_STT_BASE_URL", "STT_ADAPTER", "STT_BASE_URL");
|
|
651
493
|
}
|
|
652
494
|
function hasSynthesizerEnv(env) {
|
|
653
|
-
|
|
654
|
-
env,
|
|
655
|
-
"HAVE_SPEECH_TTS_TYPE",
|
|
656
|
-
"HAVE_SPEECH_TTS_ADAPTER",
|
|
657
|
-
"HAVE_SPEECH_TTS_BASE_URL",
|
|
658
|
-
"TTS_ADAPTER",
|
|
659
|
-
"TTS_BASE_URL"
|
|
660
|
-
);
|
|
495
|
+
return hasAnyEnv(env, "HAVE_SPEECH_TTS_TYPE", "HAVE_SPEECH_TTS_ADAPTER", "HAVE_SPEECH_TTS_BASE_URL", "TTS_ADAPTER", "TTS_BASE_URL");
|
|
661
496
|
}
|
|
662
497
|
function readEnv(env, ...keys) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return void 0;
|
|
498
|
+
for (const key of keys) {
|
|
499
|
+
const value = env[key];
|
|
500
|
+
if (value?.trim()) return value.trim();
|
|
501
|
+
}
|
|
670
502
|
}
|
|
671
503
|
function hasAnyEnv(env, ...keys) {
|
|
672
|
-
|
|
504
|
+
return keys.some((key) => Boolean(env[key]?.trim()));
|
|
673
505
|
}
|
|
674
506
|
function parseOptionalInteger(value) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const parsed = Number.parseInt(value, 10);
|
|
679
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
507
|
+
if (!value) return;
|
|
508
|
+
const parsed = Number.parseInt(value, 10);
|
|
509
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
680
510
|
}
|
|
681
511
|
function defaultEnv() {
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
SpeechProviderError,
|
|
689
|
-
getAvailableSpeechAdapters,
|
|
690
|
-
getSpeech,
|
|
691
|
-
getSpeechSynthesizer,
|
|
692
|
-
getTranscriber
|
|
693
|
-
};
|
|
694
|
-
//# sourceMappingURL=index.js.map
|
|
512
|
+
return globalThis.process?.env ?? {};
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
export { InvalidSpeechAdapterError, SpeechConfigurationError, SpeechError, SpeechProviderError, getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getTranscriber };
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=index.js.map
|