@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/tools.md
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
# Gerbil Tools & Agents
|
|
2
|
-
|
|
3
|
-
Gerbil supports tool calling with Qwen3 models, enabling agentic workflows where the LLM can call functions to accomplish tasks.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
### Simple Tool (Recommended)
|
|
8
|
-
|
|
9
|
-
Create a file in `.gerbil/tools/` with the `.tool.ts` extension:
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
// .gerbil/tools/get_weather.tool.ts
|
|
13
|
-
|
|
14
|
-
export default {
|
|
15
|
-
name: "get_weather",
|
|
16
|
-
description: "Get current weather for a city",
|
|
17
|
-
parameters: {
|
|
18
|
-
city: { type: "string", description: "City name" },
|
|
19
|
-
},
|
|
20
|
-
execute: async (params: { city: string }) => {
|
|
21
|
-
return `Weather in ${params.city}: 72°F, sunny`;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
That's it! Gerbil automatically loads tools from `.gerbil/tools/` when the REPL starts.
|
|
27
|
-
|
|
28
|
-
### Advanced Tool (with Zod)
|
|
29
|
-
|
|
30
|
-
For more complex validation, use `defineTool` with Zod schemas:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { defineTool } from "@tryhamster/gerbil";
|
|
34
|
-
import { z } from "zod";
|
|
35
|
-
|
|
36
|
-
export const weatherTool = defineTool({
|
|
37
|
-
name: "get_weather",
|
|
38
|
-
description: "Get current weather for a city",
|
|
39
|
-
parameters: z.object({
|
|
40
|
-
city: z.string().describe("City name"),
|
|
41
|
-
units: z.enum(["celsius", "fahrenheit"]).optional().default("fahrenheit"),
|
|
42
|
-
}),
|
|
43
|
-
execute: async ({ city, units }) => {
|
|
44
|
-
return `Weather in ${city}: ${units === "celsius" ? "22°C" : "72°F"}, sunny`;
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Project Tools
|
|
50
|
-
|
|
51
|
-
Store your custom tools in `.gerbil/tools/`:
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
.gerbil/
|
|
55
|
-
└── tools/
|
|
56
|
-
├── get_weather.tool.ts
|
|
57
|
-
├── search_docs.tool.ts
|
|
58
|
-
└── calculator.tool.ts
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Tool File Format
|
|
62
|
-
|
|
63
|
-
Each `.tool.ts` file exports a config object:
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// .gerbil/tools/my_tool.tool.ts
|
|
67
|
-
|
|
68
|
-
export default {
|
|
69
|
-
name: "my_tool", // Tool name (snake_case)
|
|
70
|
-
description: "What the tool does", // LLM uses this to decide when to call
|
|
71
|
-
parameters: { // Optional input parameters
|
|
72
|
-
param1: { type: "string", description: "First param" },
|
|
73
|
-
param2: { type: "number", optional: true },
|
|
74
|
-
},
|
|
75
|
-
execute: async (params) => {
|
|
76
|
-
// Your implementation
|
|
77
|
-
return "result string";
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Managing Tools in REPL
|
|
83
|
-
|
|
84
|
-
Press `3` or select **Tools** from the menu to:
|
|
85
|
-
- View all registered tools (builtin + project)
|
|
86
|
-
- Create new tools with the guided wizard
|
|
87
|
-
- Execute tools directly with `x`
|
|
88
|
-
- Open tool files in VS Code with `o`
|
|
89
|
-
|
|
90
|
-
## Built-in Tools
|
|
91
|
-
|
|
92
|
-
### `gerbil_docs`
|
|
93
|
-
|
|
94
|
-
Search Gerbil documentation for any topic.
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { docsTool } from "@tryhamster/gerbil";
|
|
98
|
-
|
|
99
|
-
const result = await docsTool({ query: "streaming" });
|
|
100
|
-
// Returns documentation about streaming responses
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**Available topics:**
|
|
104
|
-
- `quickstart` — Getting started
|
|
105
|
-
- `generate` — Text generation options
|
|
106
|
-
- `stream` — Streaming responses
|
|
107
|
-
- `json` — Structured JSON output
|
|
108
|
-
- `thinking` — Chain-of-thought reasoning
|
|
109
|
-
- `embed` — Embeddings
|
|
110
|
-
- `models` — Available models
|
|
111
|
-
- `ai-sdk` — Vercel AI SDK integration
|
|
112
|
-
- `next`, `express`, `react`, `hono` — Framework integrations
|
|
113
|
-
- `skills` — Skills system
|
|
114
|
-
- `cli` — CLI commands
|
|
115
|
-
- `tools` — Tool calling
|
|
116
|
-
|
|
117
|
-
## Tool Examples
|
|
118
|
-
|
|
119
|
-
### Calculator
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
// .gerbil/tools/calculator.tool.ts
|
|
123
|
-
|
|
124
|
-
export default {
|
|
125
|
-
name: "calculator",
|
|
126
|
-
description: "Perform basic math calculations",
|
|
127
|
-
parameters: {
|
|
128
|
-
expression: { type: "string", description: "Math expression like '2 + 2'" },
|
|
129
|
-
},
|
|
130
|
-
execute: async (params: { expression: string }) => {
|
|
131
|
-
try {
|
|
132
|
-
// Simple evaluator (use a proper math lib in production)
|
|
133
|
-
const result = Function(`return ${params.expression}`)();
|
|
134
|
-
return `${params.expression} = ${result}`;
|
|
135
|
-
} catch {
|
|
136
|
-
return `Error: Invalid expression "${params.expression}"`;
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### File Reader
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
// .gerbil/tools/read_file.tool.ts
|
|
146
|
-
|
|
147
|
-
import fs from "fs/promises";
|
|
148
|
-
|
|
149
|
-
export default {
|
|
150
|
-
name: "read_file",
|
|
151
|
-
description: "Read contents of a file",
|
|
152
|
-
parameters: {
|
|
153
|
-
path: { type: "string", description: "File path to read" },
|
|
154
|
-
},
|
|
155
|
-
execute: async (params: { path: string }) => {
|
|
156
|
-
try {
|
|
157
|
-
const content = await fs.readFile(params.path, "utf-8");
|
|
158
|
-
return content.slice(0, 2000); // Limit output size
|
|
159
|
-
} catch (e) {
|
|
160
|
-
return `Error reading file: ${e}`;
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### API Caller
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// .gerbil/tools/fetch_url.tool.ts
|
|
170
|
-
|
|
171
|
-
export default {
|
|
172
|
-
name: "fetch_url",
|
|
173
|
-
description: "Fetch content from a URL",
|
|
174
|
-
parameters: {
|
|
175
|
-
url: { type: "string", description: "URL to fetch" },
|
|
176
|
-
},
|
|
177
|
-
execute: async (params: { url: string }) => {
|
|
178
|
-
try {
|
|
179
|
-
const res = await fetch(params.url);
|
|
180
|
-
const text = await res.text();
|
|
181
|
-
return text.slice(0, 2000);
|
|
182
|
-
} catch (e) {
|
|
183
|
-
return `Error fetching URL: ${e}`;
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Joke Generator
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
// .gerbil/tools/generate_joke.tool.ts
|
|
193
|
-
|
|
194
|
-
export default {
|
|
195
|
-
name: "generate_joke",
|
|
196
|
-
description: "Generates a joke based on a theme",
|
|
197
|
-
parameters: {
|
|
198
|
-
theme: { type: "string", description: "The theme (optional)", optional: true },
|
|
199
|
-
},
|
|
200
|
-
execute: async (params: { theme?: string }) => {
|
|
201
|
-
const theme = params?.theme || "programming";
|
|
202
|
-
const jokes: Record<string, string> = {
|
|
203
|
-
programming: "Why do programmers prefer dark mode? Because light attracts bugs!",
|
|
204
|
-
coffee: "Why do Java developers wear glasses? Because they can't C#!",
|
|
205
|
-
ai: "Why did the neural network break up with the algorithm? Too many trust issues!",
|
|
206
|
-
};
|
|
207
|
-
return jokes[theme.toLowerCase()] || `Why did the ${theme} cross the road? To optimize the other side!`;
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
## Tool Registry API
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
import {
|
|
216
|
-
defineTool,
|
|
217
|
-
getTool,
|
|
218
|
-
listTools,
|
|
219
|
-
getToolDefinitions,
|
|
220
|
-
loadProjectTools
|
|
221
|
-
} from "@tryhamster/gerbil";
|
|
222
|
-
|
|
223
|
-
// Load tools from .gerbil/tools/
|
|
224
|
-
await loadProjectTools();
|
|
225
|
-
|
|
226
|
-
// Get tool by name
|
|
227
|
-
const tool = getTool("get_weather");
|
|
228
|
-
await tool({ city: "NYC" });
|
|
229
|
-
|
|
230
|
-
// List all tool names
|
|
231
|
-
console.log(listTools());
|
|
232
|
-
// ["gerbil_docs", "get_weather", "calculator"]
|
|
233
|
-
|
|
234
|
-
// Get all definitions (for prompt injection)
|
|
235
|
-
const definitions = getToolDefinitions();
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Using Tools in Chat
|
|
239
|
-
|
|
240
|
-
### REPL Agent Mode
|
|
241
|
-
|
|
242
|
-
The easiest way to use tools is in the REPL's Agent mode:
|
|
243
|
-
|
|
244
|
-
```bash
|
|
245
|
-
gerbil repl
|
|
246
|
-
# Press ⌘A (Cmd+A) to toggle agent mode
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
**Toggle shortcuts:**
|
|
250
|
-
- **⌘A (Cmd+A)**: Toggle agent mode on/off (works anywhere)
|
|
251
|
-
- **⌘T (Cmd+T)**: Toggle thinking mode on/off
|
|
252
|
-
|
|
253
|
-
When agent mode is enabled:
|
|
254
|
-
- Shows `+tools` badge in the header
|
|
255
|
-
- Tool calls display in cyan boxes with results
|
|
256
|
-
- Works with any chat persona
|
|
257
|
-
|
|
258
|
-
In Agent mode, the model can:
|
|
259
|
-
1. See available tools in its system prompt
|
|
260
|
-
2. Call tools using the `<tool_call>` format
|
|
261
|
-
3. Receive tool results and synthesize a response
|
|
262
|
-
|
|
263
|
-
### Programmatic Usage
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
import {
|
|
267
|
-
formatToolsForPrompt,
|
|
268
|
-
parseToolCall,
|
|
269
|
-
executeToolCall,
|
|
270
|
-
getToolDefinitions
|
|
271
|
-
} from "@tryhamster/gerbil";
|
|
272
|
-
|
|
273
|
-
// Add tools to system prompt
|
|
274
|
-
const tools = getToolDefinitions();
|
|
275
|
-
const systemPrompt = `You are a helpful assistant.\n\n${formatToolsForPrompt(tools)}`;
|
|
276
|
-
|
|
277
|
-
// Generate response
|
|
278
|
-
const response = await gerbil.generate(userQuery, { system: systemPrompt });
|
|
279
|
-
|
|
280
|
-
// Check for tool calls
|
|
281
|
-
const toolCall = parseToolCall(response.text);
|
|
282
|
-
if (toolCall) {
|
|
283
|
-
const result = await executeToolCall(toolCall.tool, toolCall.params);
|
|
284
|
-
// Continue conversation with tool result
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Best Practices
|
|
289
|
-
|
|
290
|
-
1. **Clear descriptions** — The model uses descriptions to decide when to call tools
|
|
291
|
-
2. **Handle missing params** — Use defaults for optional parameters
|
|
292
|
-
3. **Limit output size** — Long tool outputs consume context (aim for <2000 chars)
|
|
293
|
-
4. **Return strings** — Execute function must return a string
|
|
294
|
-
5. **Graceful errors** — Catch exceptions and return helpful error messages
|
|
295
|
-
6. **Snake_case names** — Use `get_weather` not `getWeather`
|
|
296
|
-
|
|
297
|
-
## Model Compatibility
|
|
298
|
-
|
|
299
|
-
Tool calling works best with:
|
|
300
|
-
- **Qwen3-0.6B** — Excellent tool calling support, fast
|
|
301
|
-
- **Qwen3-1.7B** — Better reasoning, still fast
|
|
302
|
-
- **Qwen2.5 models** — Good support
|
|
303
|
-
|
|
304
|
-
Smaller models (SmolLM2) may struggle with consistent tool call formatting.
|
package/docs/tts.md
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# Text-to-Speech
|
|
2
|
-
|
|
3
|
-
Gerbil's native WebGPU engine synthesizes speech with **Kani-TTS-2** — an LFM2-350M
|
|
4
|
-
codec-LM backbone driving NVIDIA's NeMo **NanoCodec** decoder (FSQ + causal HiFi-GAN).
|
|
5
|
-
`engine.speak()` returns **22.05 kHz mono PCM**, fully on-device, no ONNX.
|
|
6
|
-
|
|
7
|
-
> **Pre-1.0.** Kani-TTS-2 is the only TTS path. The old Kokoro / Supertonic ONNX /
|
|
8
|
-
> transformers.js lane has been removed. The `Gerbil`-class `speak()` method still works for
|
|
9
|
-
> backward compatibility but is now a thin wrapper over the native engine (see
|
|
10
|
-
> [below](#gerbil-class-speak-native-wrapper)).
|
|
11
|
-
|
|
12
|
-
## Quick Start
|
|
13
|
-
|
|
14
|
-
### Node
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import { WebGPUEngine } from "@tryhamster/gerbil/gpu";
|
|
18
|
-
|
|
19
|
-
const engine = await WebGPUEngine.create({ repo: "nineninesix/kani-tts-2-en" });
|
|
20
|
-
|
|
21
|
-
const { pcm, sampleRate, audioSeconds } = await engine.speak("Hello, I'm Gerbil!");
|
|
22
|
-
// pcm: Float32Array in [-1, 1], sampleRate === 22050
|
|
23
|
-
console.log(`${audioSeconds.toFixed(2)}s of audio`);
|
|
24
|
-
|
|
25
|
-
engine.destroy();
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Options
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
const result = await engine.speak("Speak with feeling.", {
|
|
32
|
-
languageTag: "en_us", // prepended as "{tag}: {text}" (default "en_us")
|
|
33
|
-
temperature: 1.0, // sampling temperature (default 1.0)
|
|
34
|
-
topP: 0.95, // nucleus threshold (default 0.95)
|
|
35
|
-
repetitionPenalty: 1.1, // (default 1.1)
|
|
36
|
-
maxFrames: 2000, // cap audio length (default: unbounded up to maxSeqLen)
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`speak()` requires a Kani-TTS-2 checkpoint (architecture `KaniTTS2ForCausalLM`). The
|
|
41
|
-
NanoCodec decoder checkpoint is downloaded lazily on first `speak()` call.
|
|
42
|
-
|
|
43
|
-
## Result
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
interface SpeakResult {
|
|
47
|
-
/** Mono PCM in [-1, 1]. */
|
|
48
|
-
pcm: Float32Array;
|
|
49
|
-
/** Sample rate — always 22050. */
|
|
50
|
-
sampleRate: number;
|
|
51
|
-
/** Number of audio frames decoded. */
|
|
52
|
-
frames: number;
|
|
53
|
-
/** Audio duration in seconds. */
|
|
54
|
-
audioSeconds: number;
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Standalone KaniTTS
|
|
59
|
-
|
|
60
|
-
`engine.speak()` lazily builds a `KaniTTS` engine under the hood. You can also use it
|
|
61
|
-
directly (e.g. without a separate text model):
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import { KaniTTS } from "@tryhamster/gerbil/gpu";
|
|
65
|
-
|
|
66
|
-
const tts = await KaniTTS.create({
|
|
67
|
-
repo: "nineninesix/kani-tts-2-en",
|
|
68
|
-
// codecRepo defaults to the NeMo 22 kHz NanoCodec MLX checkpoint
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const { pcm, sampleRate } = await tts.speak("Hello world!");
|
|
72
|
-
tts.destroy();
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Playing / Saving Audio
|
|
76
|
-
|
|
77
|
-
### Browser (Web Audio API)
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
const ctx = new AudioContext({ sampleRate });
|
|
81
|
-
const buffer = ctx.createBuffer(1, pcm.length, sampleRate);
|
|
82
|
-
buffer.copyToChannel(pcm, 0);
|
|
83
|
-
const source = ctx.createBufferSource();
|
|
84
|
-
source.buffer = buffer;
|
|
85
|
-
source.connect(ctx.destination);
|
|
86
|
-
source.start();
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Node (save to WAV)
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
import { writeFileSync } from "node:fs";
|
|
93
|
-
|
|
94
|
-
function saveWav(filename: string, audio: Float32Array, sampleRate: number) {
|
|
95
|
-
const buffer = Buffer.alloc(44 + audio.length * 2);
|
|
96
|
-
buffer.write("RIFF", 0);
|
|
97
|
-
buffer.writeUInt32LE(36 + audio.length * 2, 4);
|
|
98
|
-
buffer.write("WAVE", 8);
|
|
99
|
-
buffer.write("fmt ", 12);
|
|
100
|
-
buffer.writeUInt32LE(16, 16);
|
|
101
|
-
buffer.writeUInt16LE(1, 20); // PCM
|
|
102
|
-
buffer.writeUInt16LE(1, 22); // mono
|
|
103
|
-
buffer.writeUInt32LE(sampleRate, 24);
|
|
104
|
-
buffer.writeUInt32LE(sampleRate * 2, 28);
|
|
105
|
-
buffer.writeUInt16LE(2, 32);
|
|
106
|
-
buffer.writeUInt16LE(16, 34);
|
|
107
|
-
buffer.write("data", 36);
|
|
108
|
-
buffer.writeUInt32LE(audio.length * 2, 40);
|
|
109
|
-
for (let i = 0; i < audio.length; i++) {
|
|
110
|
-
const s = Math.max(-1, Math.min(1, audio[i]));
|
|
111
|
-
buffer.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
|
|
112
|
-
}
|
|
113
|
-
writeFileSync(filename, buffer);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { pcm, sampleRate } = await engine.speak("Hello!");
|
|
117
|
-
saveWav("output.wav", pcm, sampleRate); // 22.05 kHz mono
|
|
118
|
-
// macOS: execSync("afplay output.wav");
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Model
|
|
122
|
-
|
|
123
|
-
### Kani-TTS-2
|
|
124
|
-
|
|
125
|
-
- **Backbone:** LFM2-350M codec-LM (architecture `KaniTTS2ForCausalLM`)
|
|
126
|
-
- **Codec:** NVIDIA NeMo NanoCodec (FSQ + causal HiFi-GAN), validated bit-exact
|
|
127
|
-
- **Sample rate:** 22.05 kHz
|
|
128
|
-
- **Repos:** `nineninesix/kani-tts-2-en`. License varies by variant (the `kani-tts-2-en`
|
|
129
|
-
checkpoint is LFM1.0/other; a 450M variant is Apache 2.0).
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## `Gerbil`-class `speak()` (native wrapper)
|
|
134
|
-
|
|
135
|
-
> The Kokoro-82M / Supertonic-66M ONNX/transformers.js TTS lane has been removed. The
|
|
136
|
-
> `Gerbil`-class `speak()` method below now runs the native Kani-TTS-2 engine under the hood
|
|
137
|
-
> (it requires WebGPU and returns 22.05 kHz PCM). The browser `useSpeech` hook is gone — use
|
|
138
|
-
> `useTTS` from `@tryhamster/gerbil/gpu/hooks`. The AI SDK `gerbil.speech()` provider also
|
|
139
|
-
> routes through this native path.
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
import { Gerbil } from "@tryhamster/gerbil";
|
|
143
|
-
|
|
144
|
-
const g = new Gerbil();
|
|
145
|
-
const result = await g.speak("Hello!");
|
|
146
|
-
// result.audio = Float32Array, result.sampleRate = 22050 (native Kani-TTS-2)
|
|
147
|
-
```
|
package/docs/vision.md
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# Vision (Image → Text)
|
|
2
|
-
|
|
3
|
-
Gerbil's native WebGPU engine runs **vision language models** image-in → text-out. Two
|
|
4
|
-
vision towers are supported natively, both with the same `describeImage` API:
|
|
5
|
-
|
|
6
|
-
- **Qwen3.5 ViT** — bundled with `Qwen/Qwen3.5-0.8B` (the 0.8B text model's own built-in
|
|
7
|
-
vision tower). Bit-exact vs HuggingFace transformers.
|
|
8
|
-
- **Gemma 4 ViT** — bundled with `mlx-community/gemma-4-e2b-it-4bit`.
|
|
9
|
-
|
|
10
|
-
There is no separate "vision model" — vision is the text model's own ViT, loaded on
|
|
11
|
-
demand with `enableVision: true`.
|
|
12
|
-
|
|
13
|
-
> **Pre-1.0.** This is the native engine surface. The legacy `Gerbil` class (ONNX /
|
|
14
|
-
> transformers.js) once exposed an `images:` array on `generate()`; that path is retired and
|
|
15
|
-
> not documented here.
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
### Node
|
|
20
|
-
|
|
21
|
-
In Node you decode the image to RGB pixels yourself (HWC layout, 0..255), then pass
|
|
22
|
-
`{ pixels, width, height }`. The engine handles smart-resize, normalization, and patchify
|
|
23
|
-
internally to match the HF image processor.
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { WebGPUEngine } from "@tryhamster/gerbil/gpu";
|
|
27
|
-
|
|
28
|
-
const engine = await WebGPUEngine.create({
|
|
29
|
-
repo: "Qwen/Qwen3.5-0.8B", // BF16 checkpoint ships the ViT weights
|
|
30
|
-
enableVision: true,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// pixels: Uint8ClampedArray | Uint8Array | Float32Array, RGB, length = width*height*3
|
|
34
|
-
const { text, tokensPerSecond } = await engine.describeImage(
|
|
35
|
-
{ pixels, width, height },
|
|
36
|
-
"What's in this image?",
|
|
37
|
-
{ maxTokens: 150 },
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
console.log(text, `(${tokensPerSecond.toFixed(1)} tok/s)`);
|
|
41
|
-
engine.destroy();
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### React (Browser)
|
|
45
|
-
|
|
46
|
-
The React hook decodes URLs and data-URLs for you, so you can pass an image source string
|
|
47
|
-
directly. Load with `enableVision: true`.
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
import { useEngine } from "@tryhamster/gerbil/gpu/hooks";
|
|
51
|
-
|
|
52
|
-
function ImageDescriber() {
|
|
53
|
-
const { describeImage, completion, isLoading, isGenerating } = useEngine({
|
|
54
|
-
model: "Qwen/Qwen3.5-0.8B",
|
|
55
|
-
enableVision: true,
|
|
56
|
-
autoLoad: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (isLoading) return <div>Loading vision model…</div>;
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div>
|
|
63
|
-
<button
|
|
64
|
-
disabled={isGenerating}
|
|
65
|
-
onClick={() =>
|
|
66
|
-
describeImage("https://example.com/photo.jpg", "Describe this image in detail")
|
|
67
|
-
}
|
|
68
|
-
>
|
|
69
|
-
Describe
|
|
70
|
-
</button>
|
|
71
|
-
<p>{completion}</p>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`describeImage` accepts either an image source string (data URL or http(s) URL) or
|
|
78
|
-
pre-decoded `{ pixels, width, height }`. The hook caps the longest side at ~448px before
|
|
79
|
-
decoding — ViT attention memory scales with patch-count², so this keeps mobile coherent.
|
|
80
|
-
|
|
81
|
-
## Supported Towers
|
|
82
|
-
|
|
83
|
-
| Tower | Load from | dtype | Notes |
|
|
84
|
-
|-------|-----------|-------|-------|
|
|
85
|
-
| Qwen3.5 ViT | `Qwen/Qwen3.5-0.8B` (`enableVision: true`) | BF16 (ships ViT) | Bit-exact vs HF (per-token cosine 1.000000) |
|
|
86
|
-
| Gemma 4 ViT | `mlx-community/gemma-4-e2b-it-4bit` (`enableVision: true`) | MLX 4-bit | Native projector + multimodal-embedder norms |
|
|
87
|
-
|
|
88
|
-
> The MLX 4-bit Qwen3.5 repo (`mlx-community/Qwen3.5-0.8B-4bit`) is text-only — load the
|
|
89
|
-
> BF16 `Qwen/Qwen3.5-0.8B` repo to get the ViT weights for vision.
|
|
90
|
-
|
|
91
|
-
## How it works
|
|
92
|
-
|
|
93
|
-
`describeImage` runs the full image-in → text-out pipeline natively:
|
|
94
|
-
|
|
95
|
-
1. **Preprocess** — pixels are smart-resized, normalized, and patchified to match the HF
|
|
96
|
-
image processor (`preprocessImage` / `preprocessImageGemma4`).
|
|
97
|
-
2. **Encode** — the ViT turns patches into merged image-embedding tokens (`encodeImage`).
|
|
98
|
-
3. **Splice** — image tokens are scattered into the `image_token_id` rows of the text
|
|
99
|
-
sequence (`EmbedSplice` in the multimodal graph).
|
|
100
|
-
4. **Decode** — Qwen3.5 uses multimodal M-RoPE positions; Gemma 4 uses standard sequential
|
|
101
|
-
1D RoPE. The LM then generates the description.
|
|
102
|
-
|
|
103
|
-
The low-level pieces are exposed if you need them: `engine.encodeImage(patches, gridTHW)`
|
|
104
|
-
returns the merged image tokens, and `engine.hasVision` reports whether the engine was
|
|
105
|
-
built with a ViT.
|
|
106
|
-
|
|
107
|
-
## Image input
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// Pre-decoded RGB pixels (Node, or browser if you've already decoded)
|
|
111
|
-
await engine.describeImage({ pixels, width, height }, prompt);
|
|
112
|
-
|
|
113
|
-
// Already-built patch tensor + grid (skips host preprocessing — for reference parity)
|
|
114
|
-
await engine.describeImage({ patches, gridTHW: [1, gridH, gridW] }, prompt);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
In React, the hook also accepts a source string:
|
|
118
|
-
|
|
119
|
-
```tsx
|
|
120
|
-
describeImage("data:image/png;base64,iVBOR…", "What is this?");
|
|
121
|
-
describeImage("https://example.com/photo.jpg", "Describe it"); // must be CORS-accessible
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Options
|
|
125
|
-
|
|
126
|
-
`describeImage(image, prompt?, options?)` takes the standard generation options:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
await engine.describeImage(image, "Describe this image.", {
|
|
130
|
-
maxTokens: 150,
|
|
131
|
-
sampling: { temperature: 0.7 },
|
|
132
|
-
stopSequences: ["\n\n"],
|
|
133
|
-
onToken: (t) => process.stdout.write(t),
|
|
134
|
-
});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Returns a `GenerateResult`: `{ text, tokensGenerated, tokensPerSecond, totalTime, finishReason }`.
|
|
138
|
-
|
|
139
|
-
## Performance Tips
|
|
140
|
-
|
|
141
|
-
- **Resize before sending.** ViT attention cost grows with the square of the patch count.
|
|
142
|
-
The React hook already caps the longest side at 448px; in Node, downscale large photos
|
|
143
|
-
(≈448–512px longest side) before decoding to pixels.
|
|
144
|
-
- **Mobile.** On iOS/iPadOS the engine reserves fewer vision patches by default to stay
|
|
145
|
-
under the WebKit GPU watchdog and memory budget; very large images may still need a
|
|
146
|
-
smaller `maxVisionPatches` (a `WebGPUEngine.create` option).
|
|
147
|
-
|
|
148
|
-
## Troubleshooting
|
|
149
|
-
|
|
150
|
-
### "describeImage() requires a vision encoder"
|
|
151
|
-
|
|
152
|
-
Load with `{ enableVision: true }` on a vision-capable checkpoint (Qwen3.5 BF16 or Gemma 4).
|
|
153
|
-
The text-only MLX-4bit Qwen3.5 repo does not ship the ViT weights.
|
|
154
|
-
|
|
155
|
-
### Out of memory on mobile
|
|
156
|
-
|
|
157
|
-
Use a smaller image and/or lower `maxVisionPatches` at create time. WebKit kills the page
|
|
158
|
-
content process around 1.5–2 GB.
|