@tryhamster/gerbil 1.1.2 → 1.1.3
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 +18 -16
- package/dist/cli.mjs +1 -1
- package/dist/cli.mjs.map +1 -1
- package/dist/gpu/hooks.d.mts +3 -3
- package/dist/gpu/hooks.mjs +5 -5
- package/dist/gpu/hooks.mjs.map +1 -1
- package/dist/index-Dgmb2kE3.d.mts.map +1 -1
- package/package.json +1 -2
- package/docs/PROJECT-STATE.md +0 -321
- package/docs/adding-a-model-family.md +0 -280
- package/docs/ai-sdk.md +0 -205
- package/docs/architecture/README.md +0 -84
- package/docs/architecture/caching.md +0 -227
- package/docs/architecture/inference.md +0 -176
- package/docs/architecture/overview.md +0 -189
- package/docs/architecture/streaming.md +0 -261
- package/docs/architecture/webgpu.md +0 -213
- package/docs/browser.md +0 -762
- package/docs/cli.md +0 -155
- package/docs/embeddings.md +0 -156
- package/docs/frameworks.md +0 -90
- package/docs/gerbil-site-native-migration.md +0 -217
- package/docs/gpu-engine/architectures.md +0 -398
- package/docs/gpu-engine/ir.md +0 -372
- package/docs/gpu-engine/kernels.md +0 -718
- package/docs/gpu-engine/paper.html +0 -1759
- package/docs/gpu-engine/paper.md +0 -2109
- package/docs/gpu-engine/safetensors.md +0 -312
- package/docs/gpu-engine/tokenizer.md +0 -302
- package/docs/kernel-research-queue.md +0 -85
- package/docs/mcp-client.md +0 -224
- package/docs/mcp.md +0 -109
- package/docs/memory-rag.md +0 -91
- package/docs/memory.md +0 -301
- package/docs/metal-safari-intel.md +0 -190
- package/docs/mobile-failure-diagnosis.md +0 -124
- package/docs/mobile.md +0 -99
- package/docs/observability.md +0 -230
- package/docs/onnx-removal-plan.md +0 -339
- package/docs/repl.md +0 -473
- package/docs/research/autoresearch-portable.md +0 -933
- package/docs/research/dispatch-reduction-hivemind.md +0 -84
- package/docs/research/ios-safari-model-caching.md +0 -117
- package/docs/research/mobile-webgpu-speed-fusion.md +0 -135
- package/docs/research/native-stt-model-selection.md +0 -49
- package/docs/research/native-tts-model-selection.md +0 -90
- package/docs/research/native-vs-chromium-decision.md +0 -152
- package/docs/research/nemotron-mamba2-inference.md +0 -910
- package/docs/research/qwen35-multimodal.md +0 -293
- package/docs/research/qwen36-gemma4-targets.md +0 -337
- package/docs/research/sota-embedding-models.md +0 -179
- package/docs/research/sota-mobile-models-2026.md +0 -263
- package/docs/research/sota-modality-models.md +0 -202
- package/docs/research/tps-baselines.md +0 -71
- package/docs/research/webgpu-m4-reference.md +0 -104
- package/docs/site-update-plan.md +0 -155
- package/docs/skills.md +0 -261
- package/docs/structured-output.md +0 -123
- package/docs/stt.md +0 -111
- package/docs/tools.md +0 -304
- package/docs/tts.md +0 -147
- package/docs/vision.md +0 -158
package/docs/browser.md
DELETED
|
@@ -1,762 +0,0 @@
|
|
|
1
|
-
# Browser Usage
|
|
2
|
-
|
|
3
|
-
Run models directly in the browser with WebGPU. No server required.
|
|
4
|
-
|
|
5
|
-
Browser inference runs on the **native engine** — the React hooks at
|
|
6
|
-
`@tryhamster/gerbil/gpu/hooks` (`useEngine` / `useChat` / `useText` / `useVision` /
|
|
7
|
-
`useEmbedding` / `useTTS` / `useSTT` / `useVoiceChat` / `useMemory`),
|
|
8
|
-
backed by the from-scratch WGSL `WebGPUEngine`. Pure compute shaders, no ONNX, no
|
|
9
|
-
transformers.js. This is the supported path for text, vision, embeddings, and speech, and the
|
|
10
|
-
lane the Gerbil site itself runs on.
|
|
11
|
-
|
|
12
|
-
> The old inline transformers.js/ONNX worker hooks (`useChat`, `useSpeech`, `useVoiceInput`,
|
|
13
|
-
> `useEmbedding`, `createGerbilWorker`, `preload*`) have been **removed** from
|
|
14
|
-
> `@tryhamster/gerbil/browser`. `@tryhamster/gerbil/browser` now exports only device/WebGPU
|
|
15
|
-
> utilities (`isModelSafeForDevice`, `detectMemoryCrash`, `downloadModelChunked`,
|
|
16
|
-
> `checkWebGPUCapabilities`, `getBrowserDiagnostics`, …). The "Legacy Worker Lane" sections
|
|
17
|
-
> below are retained for historical reference and no longer reflect the shipped API.
|
|
18
|
-
|
|
19
|
-
> **Pre-1.0.** APIs may still shift before 1.0.
|
|
20
|
-
|
|
21
|
-
## Native Engine (recommended)
|
|
22
|
-
|
|
23
|
-
```tsx
|
|
24
|
-
import { useEngine } from "@tryhamster/gerbil/gpu/hooks";
|
|
25
|
-
|
|
26
|
-
function Chat() {
|
|
27
|
-
const { complete, completion, isLoading, isGenerating, tps } = useEngine({
|
|
28
|
-
model: "mlx-community/Qwen3.5-0.8B-4bit",
|
|
29
|
-
autoLoad: true, // dtype defaults to "auto": int4 on mobile, native on desktop
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (isLoading) return <div>Loading model…</div>;
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div>
|
|
36
|
-
<button onClick={() => complete("Write a haiku about coding")} disabled={isGenerating}>
|
|
37
|
-
Generate
|
|
38
|
-
</button>
|
|
39
|
-
<p>{completion}</p>
|
|
40
|
-
{isGenerating && <span>{tps?.toFixed(1)} tok/s</span>}
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`useEngine` owns the engine lifecycle — load, unload, hot-swap on config change, and
|
|
47
|
-
**reference-counted sharing** so multiple components requesting the same
|
|
48
|
-
`model|dtype|vision|embedding|maxSeqLen` share ONE engine (weights uploaded to the GPU once).
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
const {
|
|
52
|
-
complete, // (prompt, opts?) => Promise<string> — streams into `completion`
|
|
53
|
-
describeImage, // (image, prompt?, opts?) => Promise<string> — needs enableVision
|
|
54
|
-
embed, // (text, { taskType }?) => Promise<Float32Array> — needs embedding
|
|
55
|
-
similarity, // (a, b) => Promise<number> — needs embedding
|
|
56
|
-
completion, // string — current text, streams token by token
|
|
57
|
-
isLoading, loadingProgress, isGenerating, isReady, tps, error, errorKind,
|
|
58
|
-
load, stop, dispose,
|
|
59
|
-
} = useEngine({
|
|
60
|
-
model: "mlx-community/Qwen3.5-0.8B-4bit", // HF repo id
|
|
61
|
-
dtype: "auto", // "auto" (default) | "f32" | "q4"
|
|
62
|
-
maxSeqLen, // default: 2048 mobile / 4096 desktop
|
|
63
|
-
autoLoad: false, // load on mount
|
|
64
|
-
enableVision: false, // build the ViT so describeImage() works
|
|
65
|
-
embedding: false, // load as an embedding model
|
|
66
|
-
onReady, onError, // onError(err, kind) — kind: "no-webgpu" | "oom" | …
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Use `enableVision: true` for image→text (see [Vision](./vision.md)) and `embedding: true`
|
|
71
|
-
for embeddings (see [Embeddings](./embeddings.md)).
|
|
72
|
-
|
|
73
|
-
### Browser support (native engine)
|
|
74
|
-
|
|
75
|
-
- **Chrome / Edge 113+**
|
|
76
|
-
- **Safari 26+ (iOS / iPadOS 26+)**
|
|
77
|
-
- **Firefox 141+**
|
|
78
|
-
|
|
79
|
-
On devices without WebGPU the hook reports an `errorKind` of `"no-webgpu"` rather than
|
|
80
|
-
silently degrading.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Legacy Worker Lane (removed — historical reference only)
|
|
85
|
-
|
|
86
|
-
> **Removed.** Everything below this point documents the old inline transformers.js/ONNX
|
|
87
|
-
> worker hooks, which have been **deleted** from the package (no `useChat`/`useSpeech`/
|
|
88
|
-
> `useVoiceInput`/`useEmbedding` worker exports, no `createGerbilWorker`, no `preload*`
|
|
89
|
-
> functions, and no ONNX/transformers.js dependency). It is kept here only for historical
|
|
90
|
-
> reference. Use the native hooks from `@tryhamster/gerbil/gpu/hooks` instead.
|
|
91
|
-
|
|
92
|
-
## Quick Start (React)
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
import { useChat } from "@tryhamster/gerbil/browser";
|
|
96
|
-
|
|
97
|
-
function Chat() {
|
|
98
|
-
const { messages, input, setInput, handleSubmit, isLoading, isGenerating } = useChat();
|
|
99
|
-
|
|
100
|
-
if (isLoading) return <div>Loading model...</div>;
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div>
|
|
104
|
-
{messages.map(m => (
|
|
105
|
-
<div key={m.id}>
|
|
106
|
-
<strong>{m.role}:</strong> {m.content}
|
|
107
|
-
</div>
|
|
108
|
-
))}
|
|
109
|
-
<form onSubmit={handleSubmit}>
|
|
110
|
-
<input
|
|
111
|
-
value={input}
|
|
112
|
-
onChange={e => setInput(e.target.value)}
|
|
113
|
-
placeholder="Say something..."
|
|
114
|
-
/>
|
|
115
|
-
<button disabled={isGenerating}>Send</button>
|
|
116
|
-
</form>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
That's it! The hook handles model loading, streaming, and state management.
|
|
123
|
-
|
|
124
|
-
## Model Preloading
|
|
125
|
-
|
|
126
|
-
Download models during app initialization so they're ready when users need them:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
import {
|
|
130
|
-
preloadChatModel,
|
|
131
|
-
preloadEmbeddingModel,
|
|
132
|
-
preloadTTSModel,
|
|
133
|
-
preloadSTTModel
|
|
134
|
-
} from "@tryhamster/gerbil/browser";
|
|
135
|
-
|
|
136
|
-
// During app initialization
|
|
137
|
-
async function initApp() {
|
|
138
|
-
// Preload LLM
|
|
139
|
-
await preloadChatModel("qwen3-0.6b", {
|
|
140
|
-
onProgress: (p) => {
|
|
141
|
-
if (p.status === "downloading") {
|
|
142
|
-
console.log(`Downloading ${p.file}: ${p.progress}%`);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Preload other models as needed
|
|
148
|
-
await preloadEmbeddingModel("Xenova/all-MiniLM-L6-v2");
|
|
149
|
-
await preloadTTSModel("kokoro-82m");
|
|
150
|
-
await preloadSTTModel("whisper-tiny.en");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
initApp();
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
After preloading, hooks like `useChat` will load instantly from IndexedDB cache.
|
|
157
|
-
|
|
158
|
-
## React Hooks
|
|
159
|
-
|
|
160
|
-
### `useChat`
|
|
161
|
-
|
|
162
|
-
Full-featured chat hook with message history, streaming, and thinking mode.
|
|
163
|
-
|
|
164
|
-
```tsx
|
|
165
|
-
import { useChat } from "@tryhamster/gerbil/browser";
|
|
166
|
-
|
|
167
|
-
function Chat() {
|
|
168
|
-
const {
|
|
169
|
-
messages, // Message[] - chat history
|
|
170
|
-
input, // string - current input
|
|
171
|
-
setInput, // (value: string) => void
|
|
172
|
-
handleSubmit, // (e?) => void - submit handler
|
|
173
|
-
isLoading, // boolean - model loading
|
|
174
|
-
loadingProgress, // { status, file?, progress?, downloadCount? }
|
|
175
|
-
isGenerating, // boolean - generating response
|
|
176
|
-
thinking, // string - current thinking (streaming)
|
|
177
|
-
stop, // () => void - stop generation
|
|
178
|
-
clear, // () => void - clear messages
|
|
179
|
-
tps, // number - tokens per second
|
|
180
|
-
isReady, // boolean - model ready
|
|
181
|
-
error, // string | null
|
|
182
|
-
load, // () => void - manually load model
|
|
183
|
-
} = useChat({
|
|
184
|
-
model: "qwen3-0.6b", // Model ID
|
|
185
|
-
system: "You are helpful.", // System prompt
|
|
186
|
-
thinking: true, // Enable thinking mode
|
|
187
|
-
maxTokens: 512, // Max tokens per response
|
|
188
|
-
temperature: 0.7, // Sampling temperature
|
|
189
|
-
autoLoad: false, // Auto-load on mount (default: false)
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
#### Message Type
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
interface Message {
|
|
198
|
-
id: string;
|
|
199
|
-
role: "user" | "assistant";
|
|
200
|
-
content: string;
|
|
201
|
-
thinking?: string; // Thinking content (if enabled)
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
#### Auto-Loading Behavior
|
|
206
|
-
|
|
207
|
-
By default, hooks **don't** auto-load on mount (to avoid unexpected downloads). The model loads when:
|
|
208
|
-
1. You call `handleSubmit()` / `complete()` - triggers load automatically
|
|
209
|
-
2. You call `load()` manually
|
|
210
|
-
3. You set `autoLoad: true` - loads on mount
|
|
211
|
-
|
|
212
|
-
```tsx
|
|
213
|
-
// Default: loads when user submits first message
|
|
214
|
-
function Chat() {
|
|
215
|
-
const { handleSubmit, isLoading } = useChat();
|
|
216
|
-
// Model loads on first handleSubmit()
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Explicit preload on mount
|
|
220
|
-
function ChatPreloaded() {
|
|
221
|
-
const { handleSubmit, isLoading } = useChat({ autoLoad: true });
|
|
222
|
-
// Model starts loading immediately
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Manual load control
|
|
226
|
-
function ChatManual() {
|
|
227
|
-
const { load, isLoading, isReady } = useChat();
|
|
228
|
-
return <button onClick={load} disabled={isLoading || isReady}>Load Model</button>;
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
#### With Thinking Mode
|
|
233
|
-
|
|
234
|
-
```tsx
|
|
235
|
-
function ChatWithThinking() {
|
|
236
|
-
const { messages, input, setInput, handleSubmit, isLoading } = useChat({
|
|
237
|
-
thinking: true,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (isLoading) return <div>Loading...</div>;
|
|
241
|
-
|
|
242
|
-
return (
|
|
243
|
-
<div>
|
|
244
|
-
{messages.map(m => (
|
|
245
|
-
<div key={m.id}>
|
|
246
|
-
{m.thinking && (
|
|
247
|
-
<details>
|
|
248
|
-
<summary>Thinking...</summary>
|
|
249
|
-
<pre>{m.thinking}</pre>
|
|
250
|
-
</details>
|
|
251
|
-
)}
|
|
252
|
-
<p>{m.content}</p>
|
|
253
|
-
</div>
|
|
254
|
-
))}
|
|
255
|
-
<form onSubmit={handleSubmit}>
|
|
256
|
-
<input value={input} onChange={e => setInput(e.target.value)} />
|
|
257
|
-
</form>
|
|
258
|
-
</div>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
#### With Loading Progress
|
|
264
|
-
|
|
265
|
-
The `loadingProgress` object tells you what's happening:
|
|
266
|
-
|
|
267
|
-
- `status: "loading"` - Initial load / compiling shaders
|
|
268
|
-
- `status: "downloading"` - Downloading model files from network
|
|
269
|
-
- `status: "ready"` - Model ready
|
|
270
|
-
|
|
271
|
-
When loading from **cache**, you'll only see `"loading"` → `"ready"` (no `"downloading"`).
|
|
272
|
-
When **downloading**, you'll see `"downloading"` with progress info.
|
|
273
|
-
|
|
274
|
-
```tsx
|
|
275
|
-
function ChatWithProgress() {
|
|
276
|
-
const { messages, handleSubmit, isLoading, loadingProgress } = useChat();
|
|
277
|
-
|
|
278
|
-
if (isLoading) {
|
|
279
|
-
const p = loadingProgress;
|
|
280
|
-
|
|
281
|
-
if (p?.status === "downloading") {
|
|
282
|
-
// Downloading from network - show progress
|
|
283
|
-
return <div>Downloading {p.file}: {p.progress}%</div>;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Loading from cache or compiling shaders
|
|
287
|
-
return <div>{p?.message || "Loading model..."}</div>;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ... rest of chat UI
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### `useCompletion`
|
|
295
|
-
|
|
296
|
-
Simple completion hook for one-off generations.
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
import { useCompletion } from "@tryhamster/gerbil/browser";
|
|
300
|
-
|
|
301
|
-
function App() {
|
|
302
|
-
const { complete, completion, isLoading, isGenerating, tps } = useCompletion();
|
|
303
|
-
|
|
304
|
-
if (isLoading) return <div>Loading model...</div>;
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<div>
|
|
308
|
-
<button
|
|
309
|
-
onClick={() => complete("Write a haiku about coding")}
|
|
310
|
-
disabled={isGenerating}
|
|
311
|
-
>
|
|
312
|
-
Generate
|
|
313
|
-
</button>
|
|
314
|
-
<p>{completion}</p>
|
|
315
|
-
{isGenerating && <span>{tps.toFixed(0)} tok/s</span>}
|
|
316
|
-
</div>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
#### API
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
const {
|
|
325
|
-
completion, // string - generated text
|
|
326
|
-
thinking, // string - thinking content
|
|
327
|
-
complete, // (prompt: string, options?) => Promise<string>
|
|
328
|
-
isLoading, // boolean - model loading
|
|
329
|
-
loadingProgress, // { status, file?, progress? }
|
|
330
|
-
isGenerating, // boolean - generating
|
|
331
|
-
stop, // () => void - stop generation
|
|
332
|
-
tps, // number - tokens per second
|
|
333
|
-
isReady, // boolean - model ready
|
|
334
|
-
error, // string | null
|
|
335
|
-
} = useCompletion({
|
|
336
|
-
model: "qwen3-0.6b",
|
|
337
|
-
thinking: false,
|
|
338
|
-
maxTokens: 512,
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
#### Vision (Image Analysis)
|
|
343
|
-
|
|
344
|
-
> For native image→text, use `useEngine({ enableVision: true }).describeImage(...)` —
|
|
345
|
-
> see [Vision docs](./vision.md). The legacy-lane example below uses the retired ONNX worker.
|
|
346
|
-
|
|
347
|
-
Use `useCompletion` with a vision model to analyze images:
|
|
348
|
-
|
|
349
|
-
```tsx
|
|
350
|
-
import { useCompletion } from "@tryhamster/gerbil/browser";
|
|
351
|
-
|
|
352
|
-
function ImageAnalyzer() {
|
|
353
|
-
const { complete, completion, isLoading, isGenerating } = useCompletion({
|
|
354
|
-
model: "ministral-3b", // Vision model (legacy lane)
|
|
355
|
-
maxTokens: 2048,
|
|
356
|
-
});
|
|
357
|
-
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
358
|
-
|
|
359
|
-
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
360
|
-
const file = e.target.files?.[0];
|
|
361
|
-
if (file) {
|
|
362
|
-
const reader = new FileReader();
|
|
363
|
-
reader.onload = () => setImageUrl(reader.result as string);
|
|
364
|
-
reader.readAsDataURL(file);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const analyze = () => {
|
|
369
|
-
if (imageUrl) {
|
|
370
|
-
// Pass images array in the second argument
|
|
371
|
-
complete("Describe this image in detail", { images: [imageUrl] });
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
if (isLoading) return <div>Loading vision model...</div>;
|
|
376
|
-
|
|
377
|
-
return (
|
|
378
|
-
<div>
|
|
379
|
-
<input type="file" accept="image/*" onChange={handleFile} />
|
|
380
|
-
{imageUrl && <img src={imageUrl} style={{ maxWidth: 300 }} />}
|
|
381
|
-
<button onClick={analyze} disabled={!imageUrl || isGenerating}>
|
|
382
|
-
Analyze Image
|
|
383
|
-
</button>
|
|
384
|
-
<p>{completion}</p>
|
|
385
|
-
</div>
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
Images can be:
|
|
391
|
-
- **Data URIs** (`data:image/png;base64,...`) — from FileReader or canvas
|
|
392
|
-
- **HTTP URLs** — external image links (must be CORS-accessible)
|
|
393
|
-
|
|
394
|
-
Both formats work:
|
|
395
|
-
```tsx
|
|
396
|
-
// Plain strings
|
|
397
|
-
complete("Describe", { images: ["https://example.com/photo.jpg"] });
|
|
398
|
-
|
|
399
|
-
// ImageInput objects (same as core Gerbil API)
|
|
400
|
-
complete("Describe", { images: [{ source: "https://example.com/photo.jpg" }] });
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
## Voice Hooks
|
|
404
|
-
|
|
405
|
-
### `useSpeech` (TTS)
|
|
406
|
-
|
|
407
|
-
Generate speech from text in the browser:
|
|
408
|
-
|
|
409
|
-
```tsx
|
|
410
|
-
import { useSpeech } from "@tryhamster/gerbil/browser";
|
|
411
|
-
|
|
412
|
-
function SpeechDemo() {
|
|
413
|
-
const { speak, stop, isSpeaking, isLoading, listVoices } = useSpeech();
|
|
414
|
-
|
|
415
|
-
if (isLoading) return <div>Loading TTS model...</div>;
|
|
416
|
-
|
|
417
|
-
return (
|
|
418
|
-
<div>
|
|
419
|
-
<button onClick={() => speak("Hello from the browser!")}>
|
|
420
|
-
{isSpeaking ? "Speaking..." : "Speak"}
|
|
421
|
-
</button>
|
|
422
|
-
{isSpeaking && <button onClick={stop}>Stop</button>}
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
#### API
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
const {
|
|
432
|
-
speak, // (text: string, opts?) => Promise<void>
|
|
433
|
-
stop, // () => void
|
|
434
|
-
isSpeaking, // boolean
|
|
435
|
-
isLoading, // boolean
|
|
436
|
-
isReady, // boolean
|
|
437
|
-
listVoices, // () => VoiceInfo[]
|
|
438
|
-
currentVoice, // string
|
|
439
|
-
setVoice, // (id: string) => void
|
|
440
|
-
currentSpeed, // number
|
|
441
|
-
setSpeed, // (speed: number) => void
|
|
442
|
-
loadingProgress, // { status, file?, progress? }
|
|
443
|
-
error, // string | null
|
|
444
|
-
} = useSpeech({
|
|
445
|
-
voice: "af_heart", // Default voice
|
|
446
|
-
speed: 1.0, // Speed multiplier
|
|
447
|
-
autoLoad: false, // Loads on first speak()
|
|
448
|
-
});
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
📖 See [TTS docs](./tts.md) for voice list and options.
|
|
452
|
-
|
|
453
|
-
### `useVoiceInput` (STT)
|
|
454
|
-
|
|
455
|
-
Record and transcribe audio:
|
|
456
|
-
|
|
457
|
-
```tsx
|
|
458
|
-
import { useVoiceInput } from "@tryhamster/gerbil/browser";
|
|
459
|
-
|
|
460
|
-
function VoiceInput() {
|
|
461
|
-
const { startRecording, stopRecording, isRecording, transcript } = useVoiceInput({
|
|
462
|
-
onTranscript: (text) => console.log("User said:", text),
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
return (
|
|
466
|
-
<button onClick={isRecording ? stopRecording : startRecording}>
|
|
467
|
-
{isRecording ? "🔴 Stop" : "🎤 Record"}
|
|
468
|
-
</button>
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
#### Streaming Transcription (Real-time)
|
|
474
|
-
|
|
475
|
-
Transcribe audio in chunks as the user speaks - perfect for live captioning or call transcription:
|
|
476
|
-
|
|
477
|
-
```tsx
|
|
478
|
-
function LiveTranscription() {
|
|
479
|
-
const {
|
|
480
|
-
startRecording,
|
|
481
|
-
stopRecording,
|
|
482
|
-
isRecording,
|
|
483
|
-
transcript, // Full accumulated transcript
|
|
484
|
-
streamingChunk, // Current chunk being transcribed
|
|
485
|
-
chunkCount, // Number of chunks processed
|
|
486
|
-
} = useVoiceInput({
|
|
487
|
-
streaming: true, // Enable streaming mode
|
|
488
|
-
chunkDuration: 3000, // Transcribe every 3 seconds
|
|
489
|
-
onChunk: (text, idx) => console.log(`Chunk ${idx}: ${text}`),
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
return (
|
|
493
|
-
<div>
|
|
494
|
-
<button onClick={isRecording ? stopRecording : startRecording}>
|
|
495
|
-
{isRecording ? "Stop" : "Start Live Transcription"}
|
|
496
|
-
</button>
|
|
497
|
-
{streamingChunk && <p style={{ color: 'gray' }}>Current: {streamingChunk}</p>}
|
|
498
|
-
<p>Transcript: {transcript}</p>
|
|
499
|
-
</div>
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
#### API
|
|
505
|
-
|
|
506
|
-
```typescript
|
|
507
|
-
const {
|
|
508
|
-
startRecording, // () => Promise<void>
|
|
509
|
-
stopRecording, // () => Promise<string>
|
|
510
|
-
cancelRecording, // () => void
|
|
511
|
-
transcribe, // (audio: Float32Array) => Promise<string>
|
|
512
|
-
isRecording, // boolean
|
|
513
|
-
isTranscribing, // boolean
|
|
514
|
-
isLoading, // boolean
|
|
515
|
-
isReady, // boolean
|
|
516
|
-
transcript, // string - full transcript
|
|
517
|
-
streamingChunk, // string - current chunk (streaming mode)
|
|
518
|
-
chunkCount, // number - chunks processed (streaming mode)
|
|
519
|
-
loadingProgress, // { status, file?, progress? }
|
|
520
|
-
error, // string | null
|
|
521
|
-
} = useVoiceInput({
|
|
522
|
-
model: "whisper-tiny.en",
|
|
523
|
-
autoLoad: false,
|
|
524
|
-
onTranscript: (text) => {},
|
|
525
|
-
// Streaming options:
|
|
526
|
-
streaming: false, // Enable streaming mode
|
|
527
|
-
chunkDuration: 1500, // ms between transcriptions (default)
|
|
528
|
-
onChunk: (text, idx) => {}, // Called for each chunk
|
|
529
|
-
});
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
📖 See [STT docs](./stt.md) for model options.
|
|
533
|
-
|
|
534
|
-
### `useVoiceChat` (Full Voice Conversation)
|
|
535
|
-
|
|
536
|
-
Complete voice-to-voice: record → transcribe → LLM → speak:
|
|
537
|
-
|
|
538
|
-
```tsx
|
|
539
|
-
import { useVoiceChat } from "@tryhamster/gerbil/browser";
|
|
540
|
-
|
|
541
|
-
function VoiceAssistant() {
|
|
542
|
-
const {
|
|
543
|
-
messages,
|
|
544
|
-
startListening,
|
|
545
|
-
stopListening,
|
|
546
|
-
isListening,
|
|
547
|
-
isSpeaking,
|
|
548
|
-
stage, // "idle" | "listening" | "transcribing" | "thinking" | "speaking"
|
|
549
|
-
} = useVoiceChat({
|
|
550
|
-
llmModel: "qwen3-0.6b",
|
|
551
|
-
sttModel: "whisper-tiny.en",
|
|
552
|
-
voice: "af_bella",
|
|
553
|
-
system: "You are a helpful assistant.",
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
return (
|
|
557
|
-
<button
|
|
558
|
-
onMouseDown={startListening}
|
|
559
|
-
onMouseUp={stopListening}
|
|
560
|
-
>
|
|
561
|
-
{stage === "idle" ? "🎤 Hold to Speak" : stage}
|
|
562
|
-
</button>
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Audio Playback Utilities
|
|
568
|
-
|
|
569
|
-
```typescript
|
|
570
|
-
import { playAudio, createAudioPlayer } from "@tryhamster/gerbil/browser";
|
|
571
|
-
|
|
572
|
-
// One-shot playback
|
|
573
|
-
const controller = await playAudio(audioFloat32Array, 24000);
|
|
574
|
-
await controller.onEnded;
|
|
575
|
-
|
|
576
|
-
// Streaming playback
|
|
577
|
-
const player = createAudioPlayer(24000);
|
|
578
|
-
for await (const chunk of gerbil.speakStream("Long text...")) {
|
|
579
|
-
player.queue(chunk.samples);
|
|
580
|
-
}
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
## Embeddings Hook
|
|
584
|
-
|
|
585
|
-
### `useEmbedding`
|
|
586
|
-
|
|
587
|
-
Generate embeddings for semantic search and similarity:
|
|
588
|
-
|
|
589
|
-
```tsx
|
|
590
|
-
import { useEmbedding } from "@tryhamster/gerbil/browser";
|
|
591
|
-
|
|
592
|
-
function SemanticSearch() {
|
|
593
|
-
const { embed, similarity, search, isLoading, isReady, load } = useEmbedding({
|
|
594
|
-
model: "Xenova/all-MiniLM-L6-v2", // Default
|
|
595
|
-
autoLoad: false,
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
if (isLoading) return <div>Loading embedding model...</div>;
|
|
599
|
-
|
|
600
|
-
const handleSearch = async () => {
|
|
601
|
-
const results = await search("capital of France", [
|
|
602
|
-
"Paris is beautiful",
|
|
603
|
-
"London is in England",
|
|
604
|
-
"Dogs are pets",
|
|
605
|
-
], 2); // topK = 2
|
|
606
|
-
|
|
607
|
-
console.log(results);
|
|
608
|
-
// [{ text: "Paris is beautiful", score: 0.89, index: 0 }, ...]
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const handleSimilarity = async () => {
|
|
612
|
-
const score = await similarity("Hello world", "Hi there");
|
|
613
|
-
console.log(score); // 0.85
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
return (
|
|
617
|
-
<div>
|
|
618
|
-
<button onClick={handleSearch}>Search</button>
|
|
619
|
-
<button onClick={handleSimilarity}>Compare</button>
|
|
620
|
-
</div>
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
### Options
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
const {
|
|
629
|
-
// Actions
|
|
630
|
-
embed, // (text: string) => Promise<number[]>
|
|
631
|
-
embedBatch, // (texts: string[]) => Promise<{ vector, text }[]>
|
|
632
|
-
similarity, // (a: string, b: string) => Promise<number>
|
|
633
|
-
search, // (query: string, corpus: string[], topK?) => Promise<SearchResult[]>
|
|
634
|
-
findNearest, // (embedding: number[], candidates: string[], topK?) => Promise<SearchResult[]>
|
|
635
|
-
cosineSimilarity, // (a: number[], b: number[]) => number (sync)
|
|
636
|
-
load, // () => void - manually load model
|
|
637
|
-
|
|
638
|
-
// State
|
|
639
|
-
isLoading, // boolean - model loading
|
|
640
|
-
isReady, // boolean - model ready
|
|
641
|
-
loadingProgress, // { status, message?, progress? }
|
|
642
|
-
error, // string | null
|
|
643
|
-
} = useEmbedding({
|
|
644
|
-
model: "Xenova/all-MiniLM-L6-v2", // Embedding model
|
|
645
|
-
normalize: true, // Normalize vectors (default: true)
|
|
646
|
-
autoLoad: false, // Load on mount (default: false)
|
|
647
|
-
onReady: () => {},
|
|
648
|
-
onError: (err) => {},
|
|
649
|
-
});
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
## Low-Level API
|
|
653
|
-
|
|
654
|
-
For full control, use `createGerbilWorker` directly:
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
import { createGerbilWorker, isWebGPUSupported } from "@tryhamster/gerbil/browser";
|
|
658
|
-
|
|
659
|
-
if (!isWebGPUSupported()) {
|
|
660
|
-
console.log("WebGPU not supported");
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const gerbil = await createGerbilWorker({
|
|
665
|
-
modelId: "qwen3-0.6b",
|
|
666
|
-
onProgress: (p) => console.log(p.status),
|
|
667
|
-
onToken: (token) => console.log(token.text),
|
|
668
|
-
onComplete: (result) => console.log(`${result.tps} tok/s`),
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
await gerbil.generate("Hello!", { thinking: true });
|
|
672
|
-
gerbil.terminate();
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### `createGerbilWorker(options)`
|
|
676
|
-
|
|
677
|
-
```typescript
|
|
678
|
-
interface GerbilWorkerOptions {
|
|
679
|
-
modelId?: string; // Default: "qwen3-0.6b"
|
|
680
|
-
onProgress?: (progress: WorkerProgress) => void;
|
|
681
|
-
onToken?: (token: WorkerToken) => void;
|
|
682
|
-
onComplete?: (result: WorkerComplete) => void;
|
|
683
|
-
onError?: (error: string) => void;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
interface GerbilWorker {
|
|
687
|
-
generate: (prompt: string, options?: GenerateOptions) => Promise<string>;
|
|
688
|
-
interrupt: () => void;
|
|
689
|
-
reset: () => void;
|
|
690
|
-
terminate: () => void;
|
|
691
|
-
isReady: () => boolean;
|
|
692
|
-
}
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
### Generation Options
|
|
696
|
-
|
|
697
|
-
```typescript
|
|
698
|
-
interface GenerateOptions {
|
|
699
|
-
maxTokens?: number; // Default: 256
|
|
700
|
-
temperature?: number; // Default: 0.7
|
|
701
|
-
topP?: number; // Default: 0.9
|
|
702
|
-
topK?: number; // Default: 20
|
|
703
|
-
thinking?: boolean; // Enable thinking mode
|
|
704
|
-
system?: string; // System prompt
|
|
705
|
-
}
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
## Utilities
|
|
709
|
-
|
|
710
|
-
### `isWebGPUSupported()`
|
|
711
|
-
|
|
712
|
-
```typescript
|
|
713
|
-
import { isWebGPUSupported } from "@tryhamster/gerbil/browser";
|
|
714
|
-
|
|
715
|
-
if (!isWebGPUSupported()) {
|
|
716
|
-
// Show fallback or error
|
|
717
|
-
}
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### `getWebGPUInfo()`
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
import { getWebGPUInfo } from "@tryhamster/gerbil/browser";
|
|
724
|
-
|
|
725
|
-
const info = await getWebGPUInfo();
|
|
726
|
-
// { supported: true, adapter: "Apple", device: "Apple M4 Max" }
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
## Models (legacy worker lane)
|
|
730
|
-
|
|
731
|
-
| Model | Size | Best For |
|
|
732
|
-
|-------|------|----------|
|
|
733
|
-
| `qwen3-0.6b` | ~400MB | General use, thinking mode |
|
|
734
|
-
| `smollm2-360m` | ~250MB | Faster, smaller |
|
|
735
|
-
| `smollm2-135m` | ~100MB | Fastest, basic tasks |
|
|
736
|
-
|
|
737
|
-
> For vision, embeddings, and speech use the native engine (`useEngine`) — see the
|
|
738
|
-
> [Native Engine](#native-engine-recommended) section above.
|
|
739
|
-
|
|
740
|
-
Legacy-lane models are cached in IndexedDB after first download.
|
|
741
|
-
|
|
742
|
-
## Browser Support (legacy worker lane)
|
|
743
|
-
|
|
744
|
-
- **Chrome/Edge 113+** — Full WebGPU support
|
|
745
|
-
- **Safari 26+ (iOS/iPadOS 26+)** — WebGPU support
|
|
746
|
-
- **Firefox 141+** — WebGPU support
|
|
747
|
-
|
|
748
|
-
## Troubleshooting
|
|
749
|
-
|
|
750
|
-
### "WebGPU not supported"
|
|
751
|
-
|
|
752
|
-
- Update to Chrome/Edge 113+
|
|
753
|
-
- Check `chrome://gpu` for WebGPU status
|
|
754
|
-
- Try `chrome://flags/#enable-unsafe-webgpu`
|
|
755
|
-
|
|
756
|
-
### Slow first load
|
|
757
|
-
|
|
758
|
-
First load downloads the model (~400MB for qwen3-0.6b) and compiles WebGPU shaders. Subsequent loads use IndexedDB cache (~2-5s).
|
|
759
|
-
|
|
760
|
-
### Out of memory
|
|
761
|
-
|
|
762
|
-
Use smaller models like `smollm2-135m`. Close other GPU-intensive tabs.
|