@happyvertical/speech 0.110.1 → 0.111.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/AGENT.md +3 -0
- package/README.md +46 -3
- package/dist/adapters/local/audio.d.ts.map +1 -1
- package/dist/adapters/local/worker.d.ts +8 -5
- package/dist/adapters/local/worker.d.ts.map +1 -1
- package/dist/adapters/openai-compatible-transcriber.d.ts +3 -0
- package/dist/adapters/openai-compatible-transcriber.d.ts.map +1 -1
- package/dist/adapters/openai-compatible.d.ts +3 -0
- package/dist/adapters/openai-compatible.d.ts.map +1 -1
- package/dist/adapters/openai-realtime.d.ts +8 -0
- package/dist/adapters/openai-realtime.d.ts.map +1 -1
- package/dist/adapters/qwen3.d.ts +6 -0
- package/dist/adapters/qwen3.d.ts.map +1 -1
- package/dist/adapters/studio-server.d.ts +11 -0
- package/dist/adapters/studio-server.d.ts.map +1 -1
- package/dist/adapters/voxtral-realtime.d.ts +8 -0
- package/dist/adapters/voxtral-realtime.d.ts.map +1 -1
- package/dist/browser.js +2 -2
- package/dist/chunks/errors-CzsR_5dE.js +122 -0
- package/dist/chunks/errors-CzsR_5dE.js.map +1 -0
- package/dist/chunks/{registry-DFSF3WoM.js → registry-DhB2589P.js} +5 -5
- package/dist/chunks/registry-DhB2589P.js.map +1 -0
- package/dist/chunks/{wav-vzrhhZbu.js → wav-PxVZJX9V.js} +15 -5
- package/dist/chunks/{wav-vzrhhZbu.js.map → wav-PxVZJX9V.js.map} +1 -1
- package/dist/chunks/{websocket-YbAfY6YI.js → websocket-DLZLLyD9.js} +2 -2
- package/dist/chunks/{websocket-YbAfY6YI.js.map → websocket-DLZLLyD9.js.map} +1 -1
- package/dist/conversation-server.js +2 -2
- package/dist/conversation.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +302 -20
- package/dist/index.js.map +1 -1
- package/dist/local.js +21 -12
- package/dist/local.js.map +1 -1
- package/dist/pcm.js +2 -2
- package/dist/shared/audio.d.ts.map +1 -1
- package/dist/shared/errors.d.ts +48 -1
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/http.d.ts +3 -0
- package/dist/shared/http.d.ts.map +1 -1
- package/dist/shared/probe.d.ts +90 -0
- package/dist/shared/probe.d.ts.map +1 -0
- package/dist/shared/streaming-factory.d.ts.map +1 -1
- package/dist/shared/streaming-types.d.ts +10 -1
- package/dist/shared/streaming-types.d.ts.map +1 -1
- package/dist/shared/types.d.ts +14 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/wav.d.ts +12 -1
- package/dist/shared/wav.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/errors-Cs0vX-mV.js +0 -64
- package/dist/chunks/errors-Cs0vX-mV.js.map +0 -1
- package/dist/chunks/registry-DFSF3WoM.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,172 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { i as toWebSocketUrl, n as messageDataToText, r as resolveWebSocketFactory, t as isBrowserRuntime } from "./chunks/websocket-
|
|
3
|
-
import { _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, y as normalizeMimeType } from "./chunks/registry-
|
|
1
|
+
import { a as SpeechError, c as SpeechProviderUnavailableError, i as SpeechConfigurationError, l as WavFormatError, n as PcmCaptureError, o as SpeechInvalidAudioError, r as SpeechAudioTooLargeError, s as SpeechProviderError, t as InvalidSpeechAdapterError } from "./chunks/errors-CzsR_5dE.js";
|
|
2
|
+
import { i as toWebSocketUrl, n as messageDataToText, r as resolveWebSocketFactory, t as isBrowserRuntime } from "./chunks/websocket-DLZLLyD9.js";
|
|
3
|
+
import { _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, y as normalizeMimeType } from "./chunks/registry-DhB2589P.js";
|
|
4
|
+
//#region src/shared/probe.ts
|
|
5
|
+
/**
|
|
6
|
+
* Readiness probes (#1345): check that a configured speech endpoint answers,
|
|
7
|
+
* accepts the credentials, and (where the server lists models) serves the
|
|
8
|
+
* configured model, without sending audio or text to synthesize.
|
|
9
|
+
*/
|
|
10
|
+
var DEFAULT_SPEECH_PROBE_TIMEOUT_MS = 1e4;
|
|
11
|
+
/** A model list from `GET /models` is read up to this many bytes. */
|
|
12
|
+
var MAX_MODEL_LIST_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
/**
|
|
14
|
+
* Derives the OpenAI-style model list URL from a resolved endpoint: the
|
|
15
|
+
* trailing `/<resource>` is replaced with `/models` (the query is kept). An
|
|
16
|
+
* endpoint that does not end in `resource` gets `models` resolved beside it.
|
|
17
|
+
*/
|
|
18
|
+
function modelsUrlFor(endpoint, resource) {
|
|
19
|
+
const url = new URL(endpoint);
|
|
20
|
+
const suffix = `/${resource.replace(/^\/+|\/+$/g, "")}`;
|
|
21
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
22
|
+
url.pathname = pathname.endsWith(suffix) ? `${pathname.slice(0, -suffix.length)}/models` : `${pathname.replace(/\/[^/]*$/, "")}/models`;
|
|
23
|
+
url.hash = "";
|
|
24
|
+
return url.toString();
|
|
25
|
+
}
|
|
26
|
+
/** `ws:`/`wss:` to `http:`/`https:`; other URLs are returned unchanged. */
|
|
27
|
+
function toHttpUrl(url) {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
if (parsed.protocol === "ws:") parsed.protocol = "http:";
|
|
30
|
+
else if (parsed.protocol === "wss:") parsed.protocol = "https:";
|
|
31
|
+
return parsed.toString();
|
|
32
|
+
}
|
|
33
|
+
function unsupportedProbe(adapter, message) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
adapter,
|
|
37
|
+
reason: "unsupported",
|
|
38
|
+
message,
|
|
39
|
+
latencyMs: 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sends one `GET` and classifies the outcome. Resolves for every outcome
|
|
44
|
+
* except a caller abort, which rejects with the signal's reason.
|
|
45
|
+
*/
|
|
46
|
+
async function probeHttpEndpoint(target, options = {}) {
|
|
47
|
+
const startedAt = Date.now();
|
|
48
|
+
const { adapter, model } = target;
|
|
49
|
+
const fail = (reason, message, status) => ({
|
|
50
|
+
ok: false,
|
|
51
|
+
adapter,
|
|
52
|
+
reason,
|
|
53
|
+
...status !== void 0 ? { status } : {},
|
|
54
|
+
...model !== void 0 ? { model } : {},
|
|
55
|
+
message,
|
|
56
|
+
latencyMs: Date.now() - startedAt
|
|
57
|
+
});
|
|
58
|
+
const pass = (checked) => ({
|
|
59
|
+
ok: true,
|
|
60
|
+
adapter,
|
|
61
|
+
checked,
|
|
62
|
+
...model !== void 0 ? { model } : {},
|
|
63
|
+
latencyMs: Date.now() - startedAt
|
|
64
|
+
});
|
|
65
|
+
options.signal?.throwIfAborted();
|
|
66
|
+
let url;
|
|
67
|
+
let fetchImpl;
|
|
68
|
+
let headers;
|
|
69
|
+
try {
|
|
70
|
+
url = target.url();
|
|
71
|
+
fetchImpl = resolveFetch(target.fetch);
|
|
72
|
+
headers = mergeHeaders({
|
|
73
|
+
apiKey: target.apiKey,
|
|
74
|
+
headers: target.headers
|
|
75
|
+
}, options.headers);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof SpeechConfigurationError) return fail("misconfigured", error.message);
|
|
78
|
+
return fail("misconfigured", `${adapter} has an invalid header or credential`);
|
|
79
|
+
}
|
|
80
|
+
if (target.listsModels) headers.set("accept", "application/json");
|
|
81
|
+
const timeoutMs = [options.timeoutMs, target.timeoutMs].find((value) => typeof value === "number" && Number.isFinite(value) && value > 0) ?? 1e4;
|
|
82
|
+
const timeout = new AbortController();
|
|
83
|
+
const timer = setTimeout(() => timeout.abort(), timeoutMs);
|
|
84
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
|
|
85
|
+
try {
|
|
86
|
+
let response;
|
|
87
|
+
try {
|
|
88
|
+
response = await fetchImpl(url, {
|
|
89
|
+
method: "GET",
|
|
90
|
+
headers,
|
|
91
|
+
signal
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
95
|
+
if (timeout.signal.aborted) return fail("timeout", `${adapter} did not answer in ${timeoutMs} ms`);
|
|
96
|
+
return fail("unreachable", `${adapter} speech service could not be reached`);
|
|
97
|
+
}
|
|
98
|
+
if (response.status === 401 || response.status === 403) {
|
|
99
|
+
await discard(response);
|
|
100
|
+
return fail("unauthorized", `${adapter} rejected the credentials (HTTP ${response.status})`, response.status);
|
|
101
|
+
}
|
|
102
|
+
if (!target.listsModels) {
|
|
103
|
+
await discard(response);
|
|
104
|
+
return response.status < 500 ? pass("reachability") : fail("provider_error", `${adapter} answered HTTP ${response.status}`, response.status);
|
|
105
|
+
}
|
|
106
|
+
if ([
|
|
107
|
+
404,
|
|
108
|
+
405,
|
|
109
|
+
501
|
|
110
|
+
].includes(response.status)) {
|
|
111
|
+
await discard(response);
|
|
112
|
+
return pass("reachability");
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
await discard(response);
|
|
116
|
+
return fail("provider_error", `${adapter} model list failed with HTTP ${response.status}`, response.status);
|
|
117
|
+
}
|
|
118
|
+
let ids;
|
|
119
|
+
try {
|
|
120
|
+
ids = readModelIds(await readBounded(response, MAX_MODEL_LIST_BYTES));
|
|
121
|
+
} catch {
|
|
122
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
123
|
+
if (timeout.signal.aborted) return fail("timeout", `${adapter} did not answer in ${timeoutMs} ms`);
|
|
124
|
+
}
|
|
125
|
+
if (!ids) return fail("provider_error", `${adapter} model list was not an OpenAI-style { data: [{ id }] } list`, response.status);
|
|
126
|
+
if (model === void 0) return pass("reachability");
|
|
127
|
+
return ids.includes(model) ? pass("model") : fail("model_not_found", `${adapter} does not list the configured model`, response.status);
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function readModelIds(text) {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(text);
|
|
136
|
+
} catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const data = parsed?.data;
|
|
140
|
+
if (!Array.isArray(data)) return void 0;
|
|
141
|
+
return data.map((entry) => entry?.id).filter((id) => typeof id === "string");
|
|
142
|
+
}
|
|
143
|
+
async function readBounded(response, limit) {
|
|
144
|
+
if (!response.body) return response.text();
|
|
145
|
+
const reader = response.body.getReader();
|
|
146
|
+
const chunks = [];
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
total += value.byteLength;
|
|
152
|
+
if (total > limit) {
|
|
153
|
+
await reader.cancel().catch(() => void 0);
|
|
154
|
+
throw new Error("model list too large");
|
|
155
|
+
}
|
|
156
|
+
chunks.push(value);
|
|
157
|
+
}
|
|
158
|
+
const bytes = new Uint8Array(total);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const chunk of chunks) {
|
|
161
|
+
bytes.set(chunk, offset);
|
|
162
|
+
offset += chunk.byteLength;
|
|
163
|
+
}
|
|
164
|
+
return new TextDecoder().decode(bytes);
|
|
165
|
+
}
|
|
166
|
+
async function discard(response) {
|
|
167
|
+
await response.body?.cancel().catch(() => void 0);
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
4
170
|
//#region src/shared/retry.ts
|
|
5
171
|
/**
|
|
6
172
|
* Retry with exponential backoff for speech provider calls. Retries only
|
|
@@ -162,6 +328,16 @@ var HttpSpeechAdapter = class {
|
|
|
162
328
|
this.headers = options.headers;
|
|
163
329
|
this.timeoutMs = options.timeoutMs;
|
|
164
330
|
}
|
|
331
|
+
/** Runs a readiness probe with this adapter's fetch, auth and headers. */
|
|
332
|
+
probeEndpoint(target, options) {
|
|
333
|
+
return probeHttpEndpoint({
|
|
334
|
+
...target,
|
|
335
|
+
fetch: this.fetchImpl,
|
|
336
|
+
apiKey: this.apiKey,
|
|
337
|
+
headers: this.headers,
|
|
338
|
+
timeoutMs: this.timeoutMs
|
|
339
|
+
}, options);
|
|
340
|
+
}
|
|
165
341
|
/**
|
|
166
342
|
* POSTs to `path` (relative to `baseUrl`, or an absolute URL). The optional
|
|
167
343
|
* `retry` policy retries 429/5xx responses; `timeoutMs` applies per attempt.
|
|
@@ -177,15 +353,21 @@ var HttpSpeechAdapter = class {
|
|
|
177
353
|
const requestSignal = composeAbortSignals(init.signal, timeoutController?.signal);
|
|
178
354
|
try {
|
|
179
355
|
requestSignal.signal?.throwIfAborted();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
headers:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
356
|
+
let response;
|
|
357
|
+
try {
|
|
358
|
+
response = await this.fetchImpl(resolveSpeechUrl(this.baseUrl, path), {
|
|
359
|
+
...init,
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: mergeHeaders({
|
|
362
|
+
apiKey: this.apiKey,
|
|
363
|
+
headers: this.headers
|
|
364
|
+
}, init.headers),
|
|
365
|
+
signal: requestSignal.signal
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (requestSignal.signal?.aborted) throw error;
|
|
369
|
+
throw new SpeechProviderUnavailableError(adapterName, `${adapterName} speech service could not be reached`, error);
|
|
370
|
+
}
|
|
189
371
|
await assertOk(response, adapterName, this.apiKey);
|
|
190
372
|
return await readResponse(response);
|
|
191
373
|
} finally {
|
|
@@ -544,7 +726,7 @@ function unwrapRawAudio(bytes, mimeType, adapter) {
|
|
|
544
726
|
})
|
|
545
727
|
};
|
|
546
728
|
if (RAW_PCM_TYPES.has(essence)) {
|
|
547
|
-
if (essence === "audio/l16" && bytes.byteLength % 2 !== 0) throw new
|
|
729
|
+
if (essence === "audio/l16" && bytes.byteLength % 2 !== 0) throw new SpeechInvalidAudioError(`audio/L16 payload has an odd byte length (${bytes.byteLength}); 16-bit samples need an even number of bytes`, adapter);
|
|
548
730
|
return {
|
|
549
731
|
bytes: essence === "audio/l16" ? swapBytePairs(bytes) : bytes,
|
|
550
732
|
format: compact({
|
|
@@ -570,7 +752,7 @@ function unwrapRawAudio(bytes, mimeType, adapter) {
|
|
|
570
752
|
channels
|
|
571
753
|
})
|
|
572
754
|
};
|
|
573
|
-
throw new
|
|
755
|
+
throw new SpeechInvalidAudioError(`${adapter} streams raw audio and cannot decode ${essence}; send PCM WAV or raw PCM, or use an HTTP transcriber for compressed recordings`, adapter);
|
|
574
756
|
}
|
|
575
757
|
/** Returns a copy with each 16-bit sample's bytes swapped. */
|
|
576
758
|
function swapBytePairs(bytes) {
|
|
@@ -585,7 +767,7 @@ function isRiffWave(bytes) {
|
|
|
585
767
|
return bytes.byteLength >= 12 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE";
|
|
586
768
|
}
|
|
587
769
|
function parseWav(bytes, adapter) {
|
|
588
|
-
if (!isRiffWave(bytes)) throw new
|
|
770
|
+
if (!isRiffWave(bytes)) throw new SpeechInvalidAudioError("Invalid WAV header", adapter);
|
|
589
771
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
590
772
|
let offset = 12;
|
|
591
773
|
let format;
|
|
@@ -603,7 +785,7 @@ function parseWav(bytes, adapter) {
|
|
|
603
785
|
channels
|
|
604
786
|
};
|
|
605
787
|
} else if (id === "data") {
|
|
606
|
-
if (!format) throw new
|
|
788
|
+
if (!format) throw new SpeechInvalidAudioError("WAV data chunk precedes its fmt chunk", adapter);
|
|
607
789
|
const end = Math.min(bytes.byteLength, body + size);
|
|
608
790
|
return {
|
|
609
791
|
bytes: bytes.subarray(body, end),
|
|
@@ -612,13 +794,13 @@ function parseWav(bytes, adapter) {
|
|
|
612
794
|
}
|
|
613
795
|
offset = body + size + size % 2;
|
|
614
796
|
}
|
|
615
|
-
throw new
|
|
797
|
+
throw new SpeechInvalidAudioError("WAV file has no data chunk", adapter);
|
|
616
798
|
}
|
|
617
799
|
function wavEncoding(audioFormat, bitsPerSample, adapter) {
|
|
618
800
|
if ((audioFormat === 1 || audioFormat === 65534) && bitsPerSample === 16) return "pcm16";
|
|
619
801
|
if (audioFormat === 7 && bitsPerSample === 8) return "g711_ulaw";
|
|
620
802
|
if (audioFormat === 6 && bitsPerSample === 8) return "g711_alaw";
|
|
621
|
-
throw new
|
|
803
|
+
throw new SpeechInvalidAudioError(`Unsupported WAV encoding (format ${audioFormat}, ${bitsPerSample}-bit); expected 16-bit PCM or 8-bit G.711`, adapter);
|
|
622
804
|
}
|
|
623
805
|
function ascii(bytes, offset, length) {
|
|
624
806
|
return String.fromCharCode(...bytes.subarray(offset, offset + length));
|
|
@@ -1581,6 +1763,29 @@ var OpenAIRealtimeTranscriber = class {
|
|
|
1581
1763
|
resolveTurnLimit(ADAPTER$1, void 0, options);
|
|
1582
1764
|
}
|
|
1583
1765
|
/**
|
|
1766
|
+
* `GET` of the server's model list (the realtime URL's `/models`, over
|
|
1767
|
+
* HTTP), confirming the configured model is listed. Needs `apiKey` or no
|
|
1768
|
+
* auth (vLLM): a browser `clientSecret` alone cannot read the list, so the
|
|
1769
|
+
* result is then `unsupported`.
|
|
1770
|
+
*/
|
|
1771
|
+
probe(options) {
|
|
1772
|
+
if (!this.options.apiKey && this.options.clientSecret) return Promise.resolve(unsupportedProbe(ADAPTER$1, `${ADAPTER$1} with only a clientSecret cannot be probed without opening a session`));
|
|
1773
|
+
return probeHttpEndpoint({
|
|
1774
|
+
adapter: ADAPTER$1,
|
|
1775
|
+
url: () => {
|
|
1776
|
+
const url = new URL(modelsUrlFor(toHttpUrl(this.url), "realtime"));
|
|
1777
|
+
url.searchParams.delete("intent");
|
|
1778
|
+
return url.toString();
|
|
1779
|
+
},
|
|
1780
|
+
listsModels: true,
|
|
1781
|
+
model: this.options.model?.trim() || "gpt-4o-transcribe",
|
|
1782
|
+
fetch: this.options.fetch,
|
|
1783
|
+
apiKey: this.options.apiKey,
|
|
1784
|
+
headers: this.options.headers,
|
|
1785
|
+
timeoutMs: this.options.connectTimeoutMs
|
|
1786
|
+
}, options);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1584
1789
|
* A turn limit applies only to `manual` sessions with `maxTurnSeconds` set;
|
|
1585
1790
|
* under server or semantic VAD the provider ends turns itself.
|
|
1586
1791
|
*/
|
|
@@ -1822,6 +2027,29 @@ var VoxtralRealtimeTranscriber = class {
|
|
|
1822
2027
|
assertTurnDetection(options.turnDetection);
|
|
1823
2028
|
this.resolveTurnLimit();
|
|
1824
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* `GET` of the server's model list (the realtime URL's `/models`, over
|
|
2032
|
+
* HTTP), confirming the configured model is listed. Needs `apiKey` or no
|
|
2033
|
+
* auth (vLLM): a browser `clientSecret` alone cannot read the list, so the
|
|
2034
|
+
* result is then `unsupported`.
|
|
2035
|
+
*/
|
|
2036
|
+
probe(options) {
|
|
2037
|
+
if (!this.options.apiKey && this.options.clientSecret) return Promise.resolve(unsupportedProbe(ADAPTER, `${ADAPTER} with only a clientSecret cannot be probed without opening a session`));
|
|
2038
|
+
return probeHttpEndpoint({
|
|
2039
|
+
adapter: ADAPTER,
|
|
2040
|
+
url: () => {
|
|
2041
|
+
const url = new URL(modelsUrlFor(toHttpUrl(this.url), "realtime"));
|
|
2042
|
+
url.searchParams.delete("intent");
|
|
2043
|
+
return url.toString();
|
|
2044
|
+
},
|
|
2045
|
+
listsModels: true,
|
|
2046
|
+
model: this.options.model?.trim() || "mistralai/Voxtral-Mini-4B-Realtime-2602",
|
|
2047
|
+
fetch: this.options.fetch,
|
|
2048
|
+
apiKey: this.options.apiKey,
|
|
2049
|
+
headers: this.options.headers,
|
|
2050
|
+
timeoutMs: this.options.connectTimeoutMs
|
|
2051
|
+
}, options);
|
|
2052
|
+
}
|
|
1825
2053
|
turnLimit(options = {}) {
|
|
1826
2054
|
assertFormat(options.format);
|
|
1827
2055
|
return describeTurnLimit(this.resolveTurnLimit(options), VOXTRAL_REALTIME_AUDIO_FORMAT);
|
|
@@ -1911,6 +2139,15 @@ var OpenAICompatibleSpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
|
1911
2139
|
model: speech.model ?? String(payload.model)
|
|
1912
2140
|
};
|
|
1913
2141
|
}
|
|
2142
|
+
/** `GET <api root>/models`, confirming `defaultModel` is listed. */
|
|
2143
|
+
probe(options) {
|
|
2144
|
+
return this.probeEndpoint({
|
|
2145
|
+
adapter: this.type,
|
|
2146
|
+
url: () => modelsUrlFor(this.speechPath ? resolveSpeechUrl(this.baseUrl, this.speechPath) : resolveOpenAICompatibleUrl(this.baseUrl, "audio/speech"), "audio/speech"),
|
|
2147
|
+
listsModels: true,
|
|
2148
|
+
model: this.defaultModel
|
|
2149
|
+
}, options);
|
|
2150
|
+
}
|
|
1914
2151
|
};
|
|
1915
2152
|
//#endregion
|
|
1916
2153
|
//#region src/adapters/openai-compatible-transcriber.ts
|
|
@@ -2015,6 +2252,15 @@ var OpenAICompatibleTranscriber = class extends HttpSpeechAdapter {
|
|
|
2015
2252
|
timestampGranularities
|
|
2016
2253
|
};
|
|
2017
2254
|
}
|
|
2255
|
+
/** `GET <api root>/models`, confirming the configured `model` is listed. */
|
|
2256
|
+
probe(options) {
|
|
2257
|
+
return this.probeEndpoint({
|
|
2258
|
+
adapter: this.type,
|
|
2259
|
+
url: () => modelsUrlFor(this.endpoint, "audio/transcriptions"),
|
|
2260
|
+
listsModels: true,
|
|
2261
|
+
model: this.model
|
|
2262
|
+
}, options);
|
|
2263
|
+
}
|
|
2018
2264
|
};
|
|
2019
2265
|
function isUnsupportedFormatError(error, wire) {
|
|
2020
2266
|
return error instanceof SpeechProviderError && (error.status === 400 || error.status === 422) && (wire.responseFormat === "verbose_json" || Boolean(wire.timestampGranularities?.length)) && UNSUPPORTED_FORMAT_ERROR.test(error.responseBody ?? "");
|
|
@@ -2049,6 +2295,17 @@ var Qwen3SpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
|
2049
2295
|
model: speech.model ?? request.model ?? this.defaultModel
|
|
2050
2296
|
};
|
|
2051
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* `GET` of the model list beside `speechPath` (`/v1/models` by default).
|
|
2300
|
+
* The model is not required to be listed: Qwen3 servers name it freely.
|
|
2301
|
+
*/
|
|
2302
|
+
probe(options) {
|
|
2303
|
+
return this.probeEndpoint({
|
|
2304
|
+
adapter: this.type,
|
|
2305
|
+
url: () => modelsUrlFor(resolveSpeechUrl(this.baseUrl, this.speechPath), "audio/speech"),
|
|
2306
|
+
listsModels: true
|
|
2307
|
+
}, options);
|
|
2308
|
+
}
|
|
2052
2309
|
};
|
|
2053
2310
|
//#endregion
|
|
2054
2311
|
//#region src/adapters/studio-server.ts
|
|
@@ -2076,6 +2333,17 @@ var StudioServerTranscriber = class extends HttpSpeechAdapter {
|
|
|
2076
2333
|
signal: request.signal
|
|
2077
2334
|
}, (response) => readTranscriptResponse(response, this.type));
|
|
2078
2335
|
}
|
|
2336
|
+
/**
|
|
2337
|
+
* `GET` of `baseUrl`: Studio Server has no model list, so a success only
|
|
2338
|
+
* shows a server answered (`checked: 'reachability'`).
|
|
2339
|
+
*/
|
|
2340
|
+
probe(options) {
|
|
2341
|
+
return this.probeEndpoint({
|
|
2342
|
+
adapter: this.type,
|
|
2343
|
+
url: () => normalizeBaseUrl(this.baseUrl),
|
|
2344
|
+
listsModels: false
|
|
2345
|
+
}, options);
|
|
2346
|
+
}
|
|
2079
2347
|
};
|
|
2080
2348
|
var StudioServerSpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
2081
2349
|
type = "studio-server";
|
|
@@ -2093,6 +2361,17 @@ var StudioServerSpeechSynthesizer = class extends HttpSpeechAdapter {
|
|
|
2093
2361
|
signal: request.signal
|
|
2094
2362
|
}, (response) => readSynthesizedSpeechResponse(response, this.type));
|
|
2095
2363
|
}
|
|
2364
|
+
/**
|
|
2365
|
+
* `GET` of `baseUrl`: Studio Server has no model list, so a success only
|
|
2366
|
+
* shows a server answered (`checked: 'reachability'`).
|
|
2367
|
+
*/
|
|
2368
|
+
probe(options) {
|
|
2369
|
+
return this.probeEndpoint({
|
|
2370
|
+
adapter: this.type,
|
|
2371
|
+
url: () => normalizeBaseUrl(this.baseUrl),
|
|
2372
|
+
listsModels: false
|
|
2373
|
+
}, options);
|
|
2374
|
+
}
|
|
2096
2375
|
};
|
|
2097
2376
|
//#endregion
|
|
2098
2377
|
//#region src/shared/streaming-factory.ts
|
|
@@ -2190,8 +2469,10 @@ function parseMaxTurnSeconds(value) {
|
|
|
2190
2469
|
function wrapStreamingTranscriber(streaming, options = {}) {
|
|
2191
2470
|
const chunkBytes = options.chunkBytes ?? 64 * 1024;
|
|
2192
2471
|
if (!Number.isInteger(chunkBytes) || chunkBytes <= 0) throw new SpeechConfigurationError("chunkBytes must be a positive integer", streaming.type);
|
|
2472
|
+
const probe = streaming.probe?.bind(streaming);
|
|
2193
2473
|
return {
|
|
2194
2474
|
type: streaming.type,
|
|
2475
|
+
...probe ? { probe } : {},
|
|
2195
2476
|
async transcribe(request) {
|
|
2196
2477
|
const audio = await normalizeAudioInput(request.audio, {
|
|
2197
2478
|
mimeType: request.mimeType,
|
|
@@ -2308,7 +2589,8 @@ function getWrappedStreamingTranscriber(type, options, context) {
|
|
|
2308
2589
|
model: options.model,
|
|
2309
2590
|
headers: options.headers,
|
|
2310
2591
|
timeoutMs: options.timeoutMs,
|
|
2311
|
-
onUsage: options.onUsage
|
|
2592
|
+
onUsage: options.onUsage,
|
|
2593
|
+
fetch: options.fetch
|
|
2312
2594
|
})
|
|
2313
2595
|
}, {
|
|
2314
2596
|
env: context.env,
|
|
@@ -2568,6 +2850,6 @@ var ClientSecretClient = class extends HttpSpeechAdapter {
|
|
|
2568
2850
|
}
|
|
2569
2851
|
};
|
|
2570
2852
|
//#endregion
|
|
2571
|
-
export { CLIENT_SECRET_TTL_RANGE, DEFAULT_CLIENT_SECRET_TTL_SECONDS, DEFAULT_MAX_AUDIO_BYTES, DEFAULT_MAX_TURN_SECONDS, DEFAULT_ROLLOVER_MIN_SILENCE_MS, DEFAULT_ROLLOVER_SILENCE_THRESHOLD, DEFAULT_ROLLOVER_WINDOW_SECONDS, InvalidSpeechAdapterError, OPENAI_REALTIME_DEFAULT_MODEL, OPENAI_REALTIME_DEFAULT_URL, PcmCaptureError, RealtimeTranscriptionSession, STREAMING_TRANSCRIBER_ENV_KEYS, STREAMING_TRANSCRIBER_TYPES, SpeechConfigurationError, SpeechError, SpeechProviderError, TRANSCRIBER_ENV_KEYS, VOXTRAL_REALTIME_AUDIO_FORMAT, VOXTRAL_REALTIME_DEFAULT_MODEL, WavFormatError, audioSecondsForBytes, bytesPerSecond, createStreamingClientSecret, getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getStreamingTranscriber, getTranscriber, isBrowserRuntime, isStreamingTranscriberType, mimeTypeToAudioExtension, openAIRealtimeProtocol, openAIRealtimeTranscriptionSession, parseMaxTurnSeconds, parseOpenAIRealtimeEvent, parseVoxtralRealtimeEvent, resolveClientSecretUrl, resolveOpenAIRealtimeFormat, resolveRealtimeUrl, resolveVoxtralRealtimeUrl, transcriberInputFormat, unwrapRawAudio, voxtralRealtimeProtocol, wrapStreamingTranscriber };
|
|
2853
|
+
export { CLIENT_SECRET_TTL_RANGE, DEFAULT_CLIENT_SECRET_TTL_SECONDS, DEFAULT_MAX_AUDIO_BYTES, DEFAULT_MAX_TURN_SECONDS, DEFAULT_ROLLOVER_MIN_SILENCE_MS, DEFAULT_ROLLOVER_SILENCE_THRESHOLD, DEFAULT_ROLLOVER_WINDOW_SECONDS, DEFAULT_SPEECH_PROBE_TIMEOUT_MS, InvalidSpeechAdapterError, OPENAI_REALTIME_DEFAULT_MODEL, OPENAI_REALTIME_DEFAULT_URL, PcmCaptureError, RealtimeTranscriptionSession, STREAMING_TRANSCRIBER_ENV_KEYS, STREAMING_TRANSCRIBER_TYPES, SpeechAudioTooLargeError, SpeechConfigurationError, SpeechError, SpeechInvalidAudioError, SpeechProviderError, SpeechProviderUnavailableError, TRANSCRIBER_ENV_KEYS, VOXTRAL_REALTIME_AUDIO_FORMAT, VOXTRAL_REALTIME_DEFAULT_MODEL, WavFormatError, audioSecondsForBytes, bytesPerSecond, createStreamingClientSecret, getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getStreamingTranscriber, getTranscriber, isBrowserRuntime, isStreamingTranscriberType, mimeTypeToAudioExtension, openAIRealtimeProtocol, openAIRealtimeTranscriptionSession, parseMaxTurnSeconds, parseOpenAIRealtimeEvent, parseVoxtralRealtimeEvent, resolveClientSecretUrl, resolveOpenAIRealtimeFormat, resolveRealtimeUrl, resolveVoxtralRealtimeUrl, transcriberInputFormat, unwrapRawAudio, voxtralRealtimeProtocol, wrapStreamingTranscriber };
|
|
2572
2854
|
|
|
2573
2855
|
//# sourceMappingURL=index.js.map
|