@speechrouter/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -4
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,10 +1,260 @@
|
|
|
1
|
-
#
|
|
1
|
+
# speechrouter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
One API for every speech model — [speechrouter.ai](https://speechrouter.ai)
|
|
4
|
+
|
|
5
|
+
Streaming speech-to-text over WebSocket with **mid-stream provider failover**,
|
|
6
|
+
plus batch transcription. One key, one schema, 30+ models across 12 providers —
|
|
7
|
+
switch vendors by editing a string.
|
|
8
|
+
|
|
9
|
+
- **Works everywhere** — browsers, Node ≥ 18, React Native, edge runtimes
|
|
10
|
+
- **Zero runtime dependencies**
|
|
11
|
+
- **Fully typed** — every wire event and error code ships as a TypeScript type
|
|
5
12
|
|
|
6
13
|
```sh
|
|
7
14
|
npm install speechrouter
|
|
8
15
|
```
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
## Quickstart — live transcription
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { SpeechRouter } from "speechrouter";
|
|
21
|
+
|
|
22
|
+
const sr = new SpeechRouter({ apiKey: "sk_sr_..." });
|
|
23
|
+
|
|
24
|
+
const stream = sr.listen({
|
|
25
|
+
model: "deepgram/nova-3",
|
|
26
|
+
fallbacks: ["soniox/stt-rt-v5"], // primary dies mid-stream? we switch, you keep captioning
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
stream.on("open", (s) => console.log("session", s.session_id));
|
|
30
|
+
stream.on("transcript", (t) => {
|
|
31
|
+
if (t.is_final) console.log("✔", t.text);
|
|
32
|
+
else console.log("…", t.text);
|
|
33
|
+
});
|
|
34
|
+
stream.on("provider_switched", (s) =>
|
|
35
|
+
console.log(`failover: ${s.from} → ${s.to}, resumed at ${s.resumed_at}s`),
|
|
36
|
+
);
|
|
37
|
+
stream.on("error", (e) => console.error(e.code, e.message));
|
|
38
|
+
|
|
39
|
+
stream.sendAudio(pcmChunk); // 16-bit linear PCM, 16 kHz mono by default
|
|
40
|
+
|
|
41
|
+
const done = await stream.stop(); // finalize → transcript tail → usage
|
|
42
|
+
console.log(done.usage); // { audio_seconds: 12.4, model: "deepgram/nova-3" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Prefer async iteration? Every session is an `AsyncIterable` of wire events:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
for await (const event of stream) {
|
|
49
|
+
if (event.type === "transcript" && event.is_final) console.log(event.text);
|
|
50
|
+
if (event.type === "provider_switched") console.log("failover!");
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Microphone (browser)
|
|
55
|
+
|
|
56
|
+
`speechrouter/mic` captures the default mic, resamples whatever rate the
|
|
57
|
+
browser gives you (96 kHz interfaces included) down to 16 kHz PCM, and pumps
|
|
58
|
+
it into the stream:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { SpeechRouter } from "speechrouter";
|
|
62
|
+
import { openMicrophone } from "speechrouter/mic";
|
|
63
|
+
|
|
64
|
+
const stream = new SpeechRouter({ apiKey }).listen({ model: "deepgram/flux-general-en" });
|
|
65
|
+
const mic = await openMicrophone(stream, {
|
|
66
|
+
onLevel: (rms) => (meter.style.width = `${rms * 300}px`),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// later:
|
|
70
|
+
mic.stop();
|
|
71
|
+
const { usage } = await stream.stop();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It's a separate entry point so React Native and server bundles never touch
|
|
75
|
+
browser APIs.
|
|
76
|
+
|
|
77
|
+
## React
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
function Captions({ apiKey }: { apiKey: string }) {
|
|
81
|
+
const [text, setText] = useState("");
|
|
82
|
+
const [interim, setInterim] = useState("");
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const stream = new SpeechRouter({ apiKey }).listen({ model: "deepgram/nova-3" });
|
|
86
|
+
const offT = stream.on("transcript", (t) =>
|
|
87
|
+
t.is_final ? (setText((p) => p + " " + t.text), setInterim("")) : setInterim(t.text),
|
|
88
|
+
);
|
|
89
|
+
let mic: { stop(): void } | undefined;
|
|
90
|
+
void import("speechrouter/mic").then(async (m) => (mic = await m.openMicrophone(stream)));
|
|
91
|
+
return () => {
|
|
92
|
+
offT();
|
|
93
|
+
mic?.stop();
|
|
94
|
+
stream.close();
|
|
95
|
+
};
|
|
96
|
+
}, [apiKey]);
|
|
97
|
+
|
|
98
|
+
return <p>{text} <i>{interim}</i></p>;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## React Native
|
|
103
|
+
|
|
104
|
+
The core client is RN-safe (global WebSocket, no Node or DOM APIs). Capture
|
|
105
|
+
PCM with a native module and feed the stream:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import LiveAudioStream from "react-native-live-audio-stream";
|
|
109
|
+
import { Buffer } from "buffer";
|
|
110
|
+
|
|
111
|
+
LiveAudioStream.init({ sampleRate: 16000, channels: 1, bitsPerSample: 16, bufferSize: 4096 });
|
|
112
|
+
const stream = sr.listen({ model: "deepgram/nova-3" });
|
|
113
|
+
LiveAudioStream.on("data", (b64) => stream.sendAudio(Buffer.from(b64, "base64")));
|
|
114
|
+
LiveAudioStream.start();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For batch uploads, pass an RN file descriptor: `{ uri, name, type }`.
|
|
118
|
+
|
|
119
|
+
## Node
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { createReadStream } from "node:fs";
|
|
123
|
+
|
|
124
|
+
// stream a file as if it were live audio
|
|
125
|
+
const stream = sr.listen({ model: "soniox/stt-rt-v5" });
|
|
126
|
+
for await (const chunk of createReadStream("call.raw", { highWaterMark: 8000 })) {
|
|
127
|
+
stream.sendAudio(chunk);
|
|
128
|
+
await new Promise((r) => setTimeout(r, 40)); // pace ~real-time when required
|
|
129
|
+
}
|
|
130
|
+
const { usage } = await stream.stop();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Node 22+ uses the built-in WebSocket. Node 18–21: `npm install ws`
|
|
134
|
+
(optional peer dependency, picked up automatically).
|
|
135
|
+
|
|
136
|
+
## Batch transcription
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// simplest: { text }
|
|
140
|
+
const { text } = await sr.transcribe({ model: "cartesia/ink-whisper", file });
|
|
141
|
+
|
|
142
|
+
// word timings, language, duration
|
|
143
|
+
const verbose = await sr.transcribe({ model: "deepgram/nova-3", file, responseFormat: "verbose_json" });
|
|
144
|
+
|
|
145
|
+
// subtitles straight out
|
|
146
|
+
const srt = await sr.transcribe({ model: "groq/whisper-large-v3", file, responseFormat: "srt" });
|
|
147
|
+
|
|
148
|
+
// let the gateway fetch the audio itself
|
|
149
|
+
await sr.transcribe({ model: "deepgram/nova-3", url: "https://example.com/call.mp3" });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| `responseFormat` | returns |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `json` (default) | `{ text }` |
|
|
155
|
+
| `verbose_json` | text + words with timings + language + duration |
|
|
156
|
+
| `srt` / `vtt` | subtitle file as a string — synthesized from word timings even when the vendor won't |
|
|
157
|
+
| `text` | plain string |
|
|
158
|
+
|
|
159
|
+
`file` accepts a `File`/`Blob`, `Uint8Array`/`ArrayBuffer`, or a React Native
|
|
160
|
+
descriptor `{ uri, name, type }`. Files up to 250 MB.
|
|
161
|
+
|
|
162
|
+
## Models
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const models = await sr.listModels();
|
|
166
|
+
// [{ slug: "deepgram/nova-3", modes: ["streaming","batch"], pricing: {...}, capabilities: {...} }, ...]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Live catalog with pricing: [speechrouter.ai/models](https://speechrouter.ai/models)
|
|
170
|
+
|
|
171
|
+
## `listen()` options
|
|
172
|
+
|
|
173
|
+
| option | default | |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `model` | — | model slug, e.g. `"deepgram/nova-3"` |
|
|
176
|
+
| `fallbacks` | `[]` | ordered failover lane; audio is replayed into the takeover so no words are lost |
|
|
177
|
+
| `encoding` | `"linear16"` | PCM encoding of the audio you send |
|
|
178
|
+
| `sampleRate` | `16000` | sample rate of the audio you send |
|
|
179
|
+
| `channels` | `1` | channel count |
|
|
180
|
+
| `language` | auto | BCP-47 hint |
|
|
181
|
+
| `interimResults` | `true` | emit non-final hypotheses |
|
|
182
|
+
| `diarization` | `false` | speaker labels on words |
|
|
183
|
+
| `keyterms` | `[]` | bias recognition toward these terms |
|
|
184
|
+
| `includeRaw` | `false` | attach the untouched provider payload to every transcript |
|
|
185
|
+
| `providerParams` | `{}` | escape hatch: raw params forwarded to the provider |
|
|
186
|
+
| `connectTimeoutMs` | `10000` | dial timeout |
|
|
187
|
+
| `keepAlive` | `true` (8s) | keep the session alive through silences; `false` to disable — an open session bills wall-clock time on session-billed providers |
|
|
188
|
+
|
|
189
|
+
## Events
|
|
190
|
+
|
|
191
|
+
| event | payload | when |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `open` | `session_id`, `model` | gateway accepted the session |
|
|
194
|
+
| `transcript` | `is_final`, `text`, `words[]` (`w`,`start`,`end`,`conf`,`speaker`), `provider_raw?` | hypothesis or final |
|
|
195
|
+
| `provider_switched` | `from`, `to`, `resumed_at`, `speaker_mapping_preserved` | mid-stream failover happened |
|
|
196
|
+
| `done` | `usage.audio_seconds`, `usage.model` | session complete — also resolves `stream.done()` |
|
|
197
|
+
| `error` | `SpeechRouterError` | anything went wrong |
|
|
198
|
+
| `close` | `code?`, `reason?` | socket closed (always last) |
|
|
199
|
+
| `event` | any wire event | firehose — includes `speech_started`, `utterance_end`, `text.delta`, … |
|
|
200
|
+
|
|
201
|
+
**`ListenStream` methods:** `sendAudio(chunk)` (queued if the socket isn't open
|
|
202
|
+
yet) · `finalize()` · `stop()` → usage · `close()` · `done()` · `bufferedAmount`
|
|
203
|
+
· `state` · `session`.
|
|
204
|
+
|
|
205
|
+
## Errors
|
|
206
|
+
|
|
207
|
+
Everything throws or emits `SpeechRouterError`:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
try {
|
|
211
|
+
await sr.transcribe({ model: "deepgram/nova-3", file });
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e instanceof SpeechRouterError && e.code === "insufficient_credits") topUp();
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| `code` | meaning |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `auth_failed` / `key_revoked` | bad or revoked API key |
|
|
220
|
+
| `insufficient_credits` | balance empty — top up |
|
|
221
|
+
| `concurrency_exceeded` | too many simultaneous streams for your org |
|
|
222
|
+
| `model_not_found` / `unsupported_capability` / `unsupported_encoding` | request can't be routed as asked |
|
|
223
|
+
| `invalid_request` / `payload_too_large` | malformed input / file over 250 MB |
|
|
224
|
+
| `provider_error` / `provider_timeout` | upstream vendor failed (`e.provider` says which); with fallbacks these become a `provider_switched` instead |
|
|
225
|
+
| `all_providers_failed` | primary and every fallback failed |
|
|
226
|
+
| `audio_timeout` | no audio or keepalive long enough that the gateway hung up |
|
|
227
|
+
| `connection_failed` / `connection_closed` / `timeout` | client-side network conditions |
|
|
228
|
+
|
|
229
|
+
`e.recoverable` hints whether retrying the same request may succeed.
|
|
230
|
+
|
|
231
|
+
## Self-hosting
|
|
232
|
+
|
|
233
|
+
The gateway is Apache-2.0 and runs anywhere Docker runs. Point the SDK at yours:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
new SpeechRouter({ apiKey, baseUrl: "http://localhost:8080" });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Browsers & mobile: short-lived tokens
|
|
240
|
+
|
|
241
|
+
An API key shipped to a page is public — never do it. Instead, your backend
|
|
242
|
+
mints a short-lived token and hands it to the client:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// backend (key stays here)
|
|
246
|
+
const { token } = await sr.createToken({ ttlSeconds: 60 });
|
|
247
|
+
|
|
248
|
+
// client (browser / React Native)
|
|
249
|
+
const client = new SpeechRouter({ apiKey: token });
|
|
250
|
+
const stream = client.listen({ model: "deepgram/nova-3" });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The TTL only limits how long the token can *open* connections — a stream
|
|
254
|
+
that's already running continues past expiry. Default 60s, max 300s.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
Apache-2.0 · [gateway & protocol spec](https://github.com/speech-router/speechrouter) · [console](https://speechrouter.ai) · [live models & pricing](https://speechrouter.ai/models)
|
|
259
|
+
|
|
260
|
+
> `@speechrouter/sdk` is a hard re-export of [`speechrouter`](https://www.npmjs.com/package/speechrouter) — same code, same class identities. Docs use the canonical name.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speechrouter/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One API for every speech model —
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "One API for every speech model — official TypeScript SDK for speechrouter.ai. Streaming STT with mid-stream failover, batch transcription. Browser, Node, React Native.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://speechrouter.ai",
|
|
7
|
-
"repository": { "type": "git", "url": "git+https://github.com/speech-router/speechrouter.git", "directory": "packages/sdk-
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/speech-router/speechrouter.git", "directory": "packages/sdk-ts" },
|
|
8
|
+
"keywords": ["speech-to-text", "stt", "transcription", "streaming", "websocket", "deepgram", "whisper", "react-native"],
|
|
8
9
|
"type": "module",
|
|
9
10
|
"main": "./index.cjs",
|
|
10
11
|
"module": "./index.js",
|
|
@@ -14,5 +15,6 @@
|
|
|
14
15
|
"./mic": { "types": "./mic.d.ts", "import": "./mic.js", "require": "./mic.cjs" }
|
|
15
16
|
},
|
|
16
17
|
"files": ["index.js", "index.cjs", "index.d.ts", "mic.js", "mic.cjs", "mic.d.ts", "README.md"],
|
|
17
|
-
"
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"dependencies": { "speechrouter": "^0.3.0" }
|
|
18
20
|
}
|