@happyvertical/speech 0.79.0 → 0.80.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/dist/index.js CHANGED
@@ -1,694 +1,517 @@
1
- class SpeechError extends Error {
2
- constructor(message, code, adapter) {
3
- super(message);
4
- this.code = code;
5
- this.adapter = adapter;
6
- this.name = "SpeechError";
7
- }
8
- }
9
- class SpeechConfigurationError extends SpeechError {
10
- constructor(message, adapter) {
11
- super(message, "SPEECH_CONFIGURATION_ERROR", adapter);
12
- this.name = "SpeechConfigurationError";
13
- }
14
- }
15
- class InvalidSpeechAdapterError extends SpeechError {
16
- constructor(type, kind) {
17
- super(`Invalid ${kind} speech adapter type: ${type}`, "INVALID_ADAPTER");
18
- this.name = "InvalidSpeechAdapterError";
19
- }
20
- }
21
- class SpeechProviderError extends SpeechError {
22
- status;
23
- responseBody;
24
- constructor(adapter, message, options = {}) {
25
- super(message, "SPEECH_PROVIDER_ERROR", adapter);
26
- this.name = "SpeechProviderError";
27
- this.status = options.status;
28
- this.responseBody = options.responseBody;
29
- if (options.cause !== void 0) {
30
- this.cause = options.cause;
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
- if (!baseUrl.trim()) {
36
- throw new SpeechConfigurationError("Speech adapter baseUrl is required");
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
- return new URL(path.replace(/^\//, ""), normalizeBaseUrl(baseUrl)).toString();
42
+ return new URL(path.replace(/^\//, ""), normalizeBaseUrl(baseUrl)).toString();
42
43
  }
43
44
  function resolveFetch(fetchOverride) {
44
- if (fetchOverride) {
45
- return fetchOverride;
46
- }
47
- const globalFetch = globalThis.fetch;
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
- const headers = new Headers(options.headers);
57
- if (options.apiKey && !headers.has("authorization")) {
58
- headers.set("authorization", `Bearer ${options.apiKey}`);
59
- }
60
- if (extra) {
61
- new Headers(extra).forEach((value, key) => {
62
- headers.set(key, value);
63
- });
64
- }
65
- return headers;
66
- }
67
- class HttpSpeechAdapter {
68
- baseUrl;
69
- fetchImpl;
70
- apiKey;
71
- headers;
72
- timeoutMs;
73
- constructor(options) {
74
- this.baseUrl = options.baseUrl;
75
- this.fetchImpl = resolveFetch(options.fetch);
76
- this.apiKey = options.apiKey;
77
- this.headers = options.headers;
78
- this.timeoutMs = options.timeoutMs;
79
- }
80
- async post(adapterName, path, init, readResponse) {
81
- const timeoutController = this.timeoutMs && this.timeoutMs > 0 ? new AbortController() : void 0;
82
- const timeout = timeoutController && this.timeoutMs ? setTimeout(() => timeoutController.abort(), this.timeoutMs) : void 0;
83
- const requestSignal = composeAbortSignals(
84
- init.signal,
85
- timeoutController?.signal
86
- );
87
- try {
88
- const response = await this.fetchImpl(
89
- resolveSpeechUrl(this.baseUrl, path),
90
- {
91
- ...init,
92
- method: "POST",
93
- headers: mergeHeaders(
94
- { apiKey: this.apiKey, headers: this.headers },
95
- init.headers
96
- ),
97
- signal: requestSignal.signal
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
- const activeSignals = signals.filter(
112
- (signal) => Boolean(signal)
113
- );
114
- if (activeSignals.length === 0) {
115
- return { cleanup: () => void 0 };
116
- }
117
- if (activeSignals.length === 1) {
118
- return { signal: activeSignals[0], cleanup: () => void 0 };
119
- }
120
- const controller = new AbortController();
121
- let cleanup = () => void 0;
122
- const abortFrom = (signal) => {
123
- if (!controller.signal.aborted) {
124
- controller.abort(signal.reason);
125
- }
126
- cleanup();
127
- };
128
- const onAbort = (event) => {
129
- abortFrom(event.target);
130
- };
131
- cleanup = () => {
132
- for (const signal of activeSignals) {
133
- signal.removeEventListener("abort", onAbort);
134
- }
135
- };
136
- for (const signal of activeSignals) {
137
- if (signal.aborted) {
138
- abortFrom(signal);
139
- return { signal: controller.signal, cleanup: () => void 0 };
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
- if (response.ok) {
149
- return;
150
- }
151
- const responseBody = await response.text().catch(() => void 0);
152
- throw new SpeechProviderError(
153
- adapterName,
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
- const filename = audio.filename ?? "audio";
163
- const data = audio.data;
164
- if (typeof Blob !== "undefined" && data instanceof Blob) {
165
- const contentType2 = audio.contentType ?? (data.type || "application/octet-stream");
166
- const blob = data.type === contentType2 ? data : new Blob([data], { type: contentType2 });
167
- form.append(fieldName, blob, filename);
168
- return;
169
- }
170
- const contentType = audio.contentType ?? "application/octet-stream";
171
- const blobPart = data instanceof Uint8Array ? arrayBufferFromBytes(data) : data;
172
- form.append(fieldName, new Blob([blobPart], { type: contentType }), filename);
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
- if (value === void 0 || value === null) {
176
- return;
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
- const form = new FormData();
182
- const voice = request.voice && typeof request.voice !== "string" ? request.voice : void 0;
183
- form.set("text", request.text);
184
- appendOptionalFormValue(
185
- form,
186
- "language",
187
- request.language ?? voice?.language
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
- if (!voice) {
200
- return fallback;
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
- return Object.fromEntries(
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
- const match = /^audio\/([^;]+)/.exec(contentType);
214
- return match?.[1];
169
+ return /^audio\/([^;]+)/.exec(contentType)?.[1];
215
170
  }
216
171
  function arrayBufferFromBytes(bytes) {
217
- const copy = new Uint8Array(bytes.byteLength);
218
- copy.set(bytes);
219
- return copy.buffer;
172
+ const copy = new Uint8Array(bytes.byteLength);
173
+ copy.set(bytes);
174
+ return copy.buffer;
220
175
  }
221
176
  function decodeBase64(base64) {
222
- if (typeof Buffer !== "undefined") {
223
- return arrayBufferFromBytes(Buffer.from(base64, "base64"));
224
- }
225
- const binary = atob(base64);
226
- const bytes = new Uint8Array(binary.length);
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
- for (const key of keys) {
234
- const candidate = value[key];
235
- if (typeof candidate === "number") {
236
- return candidate;
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
- for (const key of keys) {
243
- const candidate = value[key];
244
- if (typeof candidate === "string") {
245
- return candidate;
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
- if (!Array.isArray(value)) {
252
- return void 0;
253
- }
254
- return value.flatMap((entry) => {
255
- if (!entry || typeof entry !== "object") {
256
- return [];
257
- }
258
- const record = entry;
259
- const word = getString(record, "word", "text", "token");
260
- const startSeconds = getNumber(
261
- record,
262
- "startSeconds",
263
- "start",
264
- "start_time"
265
- );
266
- const endSeconds = getNumber(record, "endSeconds", "end", "end_time");
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
- if (!Array.isArray(value)) {
283
- return void 0;
284
- }
285
- return value.flatMap((entry) => {
286
- if (!entry || typeof entry !== "object") {
287
- return [];
288
- }
289
- const record = entry;
290
- const text = getString(record, "text", "transcript");
291
- if (!text) {
292
- return [];
293
- }
294
- return [
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
- const contentType = response.headers.get("content-type") ?? "";
307
- if (!contentType.includes("application/json")) {
308
- const text2 = await response.text();
309
- return {
310
- text: text2,
311
- provider: adapterName
312
- };
313
- }
314
- const json = await response.json();
315
- const text = getString(json, "text", "transcript", "transcription") ?? "";
316
- return {
317
- text,
318
- language: getString(json, "language", "lang"),
319
- durationSeconds: getNumber(json, "durationSeconds", "duration"),
320
- words: normalizeWordTimings(
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
- const responseContentType = response.headers.get("content-type") ?? "application/octet-stream";
331
- if (!responseContentType.includes("application/json")) {
332
- const audio = await response.arrayBuffer();
333
- const sampleRate = parseHeaderNumber(response, "x-sample-rate");
334
- return {
335
- audio,
336
- contentType: responseContentType,
337
- format: contentTypeToFormat(responseContentType),
338
- sampleRate,
339
- provider: adapterName
340
- };
341
- }
342
- const json = await response.json();
343
- const contentType = getString(json, "contentType", "content_type", "mimeType", "mime_type") ?? "application/octet-stream";
344
- const encodedAudio = getString(
345
- json,
346
- "audio",
347
- "audioContent",
348
- "audio_content"
349
- );
350
- if (!encodedAudio) {
351
- throw new SpeechProviderError(
352
- adapterName,
353
- `${adapterName} JSON speech response did not include base64 audio`
354
- );
355
- }
356
- return {
357
- audio: decodeBase64(encodedAudio),
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
- const value = response.headers.get(headerName);
373
- if (!value) {
374
- return void 0;
375
- }
376
- const parsed = Number(value);
377
- return Number.isFinite(parsed) ? parsed : void 0;
378
- }
379
- class OpenAICompatibleSpeechSynthesizer extends HttpSpeechAdapter {
380
- type = "openai-compatible";
381
- speechPath;
382
- defaultModel;
383
- defaultVoice;
384
- constructor(options) {
385
- super(options);
386
- this.speechPath = options.speechPath ?? "/v1/audio/speech";
387
- this.defaultModel = options.defaultModel ?? "tts-1";
388
- this.defaultVoice = options.defaultVoice ?? "alloy";
389
- }
390
- async synthesize(request) {
391
- const outputFormat = request.outputFormat ?? "mp3";
392
- const payload = compactJson({
393
- model: request.model ?? this.defaultModel,
394
- input: request.text,
395
- voice: voiceToString(request.voice, this.defaultVoice),
396
- // biome-ignore lint/style/useNamingConvention: OpenAI-compatible API uses snake_case.
397
- response_format: outputFormat,
398
- speed: request.speed
399
- });
400
- const speech = await this.post(
401
- this.type,
402
- this.speechPath,
403
- {
404
- headers: {
405
- "content-type": "application/json"
406
- },
407
- body: JSON.stringify(payload),
408
- signal: request.signal
409
- },
410
- (response) => readSynthesizedSpeechResponse(response, this.type)
411
- );
412
- return {
413
- ...speech,
414
- format: speech.format ?? outputFormat,
415
- model: speech.model ?? String(payload.model)
416
- };
417
- }
418
- }
419
- class Qwen3SpeechSynthesizer extends HttpSpeechAdapter {
420
- type = "qwen3-tts";
421
- speechPath;
422
- defaultModel;
423
- defaultVoice;
424
- constructor(options) {
425
- super(options);
426
- this.speechPath = options.speechPath ?? "/v1/audio/speech";
427
- this.defaultModel = options.defaultModel ?? "qwen3-tts";
428
- this.defaultVoice = options.defaultVoice;
429
- }
430
- async synthesize(request) {
431
- const form = createHappyVerticalSynthesisForm(request, this.defaultVoice);
432
- const speech = await this.post(
433
- this.type,
434
- this.speechPath,
435
- {
436
- body: form,
437
- signal: request.signal
438
- },
439
- (response) => readSynthesizedSpeechResponse(response, this.type)
440
- );
441
- return {
442
- ...speech,
443
- format: speech.format ?? request.outputFormat,
444
- model: speech.model ?? request.model ?? this.defaultModel
445
- };
446
- }
447
- }
448
- class StudioServerTranscriber extends HttpSpeechAdapter {
449
- type = "studio-server";
450
- transcribePath;
451
- constructor(options) {
452
- super(options);
453
- this.transcribePath = options.transcribePath ?? "/v1/transcribe";
454
- }
455
- async transcribe(request) {
456
- const form = new FormData();
457
- appendAudioInput(form, request.audio);
458
- appendOptionalFormValue(form, "language", request.language);
459
- return this.post(
460
- this.type,
461
- this.transcribePath,
462
- {
463
- body: form,
464
- signal: request.signal
465
- },
466
- (response) => readTranscriptResponse(response, this.type)
467
- );
468
- }
469
- }
470
- class StudioServerSpeechSynthesizer extends HttpSpeechAdapter {
471
- type = "studio-server";
472
- synthesizePath;
473
- defaultVoice;
474
- constructor(options) {
475
- super(options);
476
- this.synthesizePath = options.synthesizePath ?? "/v1/tts/synthesize";
477
- this.defaultVoice = options.defaultVoice;
478
- }
479
- async synthesize(request) {
480
- const form = createHappyVerticalSynthesisForm(request, this.defaultVoice);
481
- return this.post(
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
- const transcriber = options.transcriber === false ? void 0 : await getOptionalTranscriber(options.transcriber, context);
494
- const synthesizer = options.synthesizer === false ? void 0 : await getOptionalSpeechSynthesizer(options.synthesizer, context);
495
- return {
496
- transcriber,
497
- synthesizer,
498
- async transcribe(request) {
499
- if (!transcriber) {
500
- throw new SpeechConfigurationError("No STT provider configured");
501
- }
502
- return transcriber.transcribe(request);
503
- },
504
- async synthesize(request) {
505
- if (!synthesizer) {
506
- throw new SpeechConfigurationError("No TTS provider configured");
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
- const resolved = normalizeTranscriberOptions(options, context);
514
- switch (resolved.type) {
515
- case "studio-server":
516
- return new StudioServerTranscriber(resolved);
517
- default:
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
- const resolved = normalizeSpeechSynthesizerOptions(options, context);
526
- switch (resolved.type) {
527
- case "studio-server":
528
- return new StudioServerSpeechSynthesizer(resolved);
529
- case "qwen3-tts":
530
- return new Qwen3SpeechSynthesizer(resolved);
531
- case "openai-compatible":
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
- return {
542
- transcribers: ["studio-server"],
543
- synthesizers: ["studio-server", "qwen3-tts", "openai-compatible"]
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
- if (!options && !hasTranscriberEnv(context.env ?? defaultEnv())) {
548
- return void 0;
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
- if (!options && !hasSynthesizerEnv(context.env ?? defaultEnv())) {
554
- return void 0;
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
- const env = context.env ?? defaultEnv();
560
- const type = options.type ?? readEnv(
561
- env,
562
- "HAVE_SPEECH_STT_TYPE",
563
- "HAVE_SPEECH_STT_ADAPTER",
564
- "STT_ADAPTER"
565
- ) ?? "studio-server";
566
- const baseUrl = options.baseUrl ?? readEnv(env, "HAVE_SPEECH_STT_BASE_URL", "STT_BASE_URL");
567
- if (type !== "studio-server") {
568
- throw new InvalidSpeechAdapterError(type, "STT");
569
- }
570
- if (!baseUrl?.trim()) {
571
- throw new SpeechConfigurationError("STT baseUrl is required", type);
572
- }
573
- return {
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
- const env = context.env ?? defaultEnv();
588
- const type = options?.type ?? readEnv(
589
- env,
590
- "HAVE_SPEECH_TTS_TYPE",
591
- "HAVE_SPEECH_TTS_ADAPTER",
592
- "TTS_ADAPTER"
593
- );
594
- const baseUrl = options?.baseUrl ?? readEnv(env, "HAVE_SPEECH_TTS_BASE_URL", "TTS_BASE_URL");
595
- if (!type) {
596
- throw new SpeechConfigurationError("TTS provider type is required");
597
- }
598
- if (type !== "studio-server" && type !== "qwen3-tts" && type !== "openai-compatible") {
599
- throw new InvalidSpeechAdapterError(type, "TTS");
600
- }
601
- if (!baseUrl?.trim()) {
602
- throw new SpeechConfigurationError("TTS baseUrl is required", type);
603
- }
604
- const shared = {
605
- type,
606
- baseUrl: baseUrl.trim(),
607
- fetch: options?.fetch ?? context.fetch,
608
- headers: options?.headers ?? context.headers,
609
- apiKey: options?.apiKey ?? readEnv(env, "HAVE_SPEECH_TTS_API_KEY", "TTS_API_KEY", "SPEECH_API_KEY"),
610
- timeoutMs: options?.timeoutMs ?? parseOptionalInteger(
611
- readEnv(env, "HAVE_SPEECH_TTS_TIMEOUT_MS", "TTS_TIMEOUT_MS")
612
- )
613
- };
614
- if (type === "studio-server") {
615
- return {
616
- ...shared,
617
- type,
618
- synthesizePath: options?.synthesizePath ?? readEnv(env, "HAVE_SPEECH_TTS_PATH", "TTS_PATH"),
619
- defaultVoice: options?.defaultVoice ?? readEnv(env, "HAVE_SPEECH_TTS_VOICE", "TTS_VOICE")
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
- return hasAnyEnv(
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
- return hasAnyEnv(
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
- for (const key of keys) {
664
- const value = env[key];
665
- if (value?.trim()) {
666
- return value.trim();
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
- return keys.some((key) => Boolean(env[key]?.trim()));
504
+ return keys.some((key) => Boolean(env[key]?.trim()));
673
505
  }
674
506
  function parseOptionalInteger(value) {
675
- if (!value) {
676
- return void 0;
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
- return globalThis.process?.env ?? {};
683
- }
684
- export {
685
- InvalidSpeechAdapterError,
686
- SpeechConfigurationError,
687
- SpeechError,
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