@degreesign/chat 1.1.0 → 1.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 CHANGED
@@ -1,14 +1,21 @@
1
1
  # @degreesign/chat
2
2
 
3
- One-shot on-device AI chat for browsers, workers and Node, running a wllama CPU
4
- engine or a LiteRT-LM WebGPU engine. The host owns the messages and the UI; the
5
- package loads one model, streams one reply and reports status.
3
+ One-shot on-device AI chat for browsers, workers and Node, using wllama CPU
4
+ engine and LiteRT-LM WebGPU engine.
5
+ The package is an A-Z solution for local AI chat and more.
6
+ Includes voice transcription to many languages
7
+ and a super accurate image vision function.
8
+ The package manages download of AI models automatically,
9
+ with auto resume for low network connections.
10
+
11
+ `@degreesign/chat` is plug and play solution for any UI.
6
12
 
7
13
  [![npm version](https://img.shields.io/npm/v/@degreesign/chat.svg)](https://www.npmjs.com/package/@degreesign/chat)
8
14
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](#licensing)
9
15
  [![node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](#faq)
10
16
  [![types](https://img.shields.io/badge/types-included-brightgreen.svg)](#faq)
11
17
  [![architecture](https://img.shields.io/badge/architecture-view-blue.svg)](https://degreesign.github.io/ds_chat/architecture.html)
18
+ [![comparison](https://img.shields.io/badge/comparison-web--llm-blue.svg)](https://degreesign.github.io/ds_chat/vs_web_llm.html)
12
19
 
13
20
  ## Table of contents
14
21
 
@@ -25,6 +32,7 @@ package loads one model, streams one reply and reports status.
25
32
  - [Node.js](#nodejs)
26
33
  - [React](#react)
27
34
  - [Types](#types)
35
+ - [Testing](#testing)
28
36
  - [Architecture](#architecture)
29
37
  - [FAQ](#faq)
30
38
  - [Licensing](#licensing)
@@ -39,7 +47,7 @@ package loads one model, streams one reply and reports status.
39
47
  - Images: a user turn can carry image URLs, sent only to a vision model and
40
48
  capped by the prompt budgets.
41
49
  - Audio: a user turn can carry an audio URL, sent only to a model with audio
42
- support and capped by the prompt budgets.
50
+ support. No model in the catalog has an audio encoder.
43
51
  - Reusable: the loaded model stays in memory for the next call.
44
52
  - Typed: written in TypeScript with declarations for every export.
45
53
  - Portable: importing and calling works in browsers, workers, React and Node.
@@ -126,38 +134,28 @@ second, measured from the first token so prompt processing is excluded.
126
134
  Attach images to a user turn with `images: [dataUrl]`. The package sends them
127
135
  only to a model with vision support, fits each image to 128 to 136 vision
128
136
  tokens, snaps both sides to multiples of 32 px, crops the remainder, and caps
129
- the number with `limits.maxImages`. The WebGPU engine asks LiteRT for the
130
- smallest Gemma 4 vision budget (70 tokens, 630 patches), so it center-crops to a
131
- 384 x 384 square instead. On a phone or tablet a Gemma request resolves to
132
- `gemma4_e2b_limited`, the text-only browser build, so images there go through
133
- `qwen3_vl_2b` instead.
134
-
135
- Attach a recording with `audio: [dataUrl]`. Gemma 4 E2B accepts WAV, MP3 and
136
- FLAC on the desktop WebGPU engine and loads its audio encoder on demand. A short
137
- 16 kHz mono WAV is the best input. `limits.maxAudio` caps the clips (default 1).
138
- A model without audio drops the clip when the turn also has text; an audio-only
139
- turn reports `Model does not accept audio`. On a phone or tablet
140
- `gemma4_e2b_limited` is text only, so audio follows the same rule as images
141
- there. The package does not transcode, so the host supplies one of those
142
- formats.
137
+ the number with `limits.maxImages`. `qwen3_vl_2b` is the vision model in the
138
+ catalog, and it runs on the CPU engine. A turn sent to a model without vision
139
+ drops its images when the turn also has text, and an image-only turn reports
140
+ `Model does not accept images`.
141
+
142
+ Attach a recording with `audio: [dataUrl]`. No model in the catalog has an
143
+ audio encoder, so a text turn drops the clip and an audio-only turn reports
144
+ `Model does not accept audio`. `limits.maxAudio` still caps the clips
145
+ (default 1).
143
146
 
144
147
  The package never writes storage, so the host appends the user message before
145
148
  the call and appends the assistant message after the last `onResponse`.
146
149
 
147
150
  ## Enable WebGPU
148
151
 
149
- A request without `model` picks Gemma when the browser exposes a WebGPU adapter
150
- and `qwen3_vl_2b` when it does not. A phone or tablet asking for Gemma gets
151
- `gemma4_e2b_limited`, the 2.0 GB text-only browser build
152
- (`gemma-4-E2B-it-gpu.litertlm`); a desktop gets `gemma4_e2b`, the 2.6 GB
153
- multimodal build with vision and audio. The downgrade applies to `gemma4_e2b`
154
- alone: an explicit `gemma4_e2b_limited` loads the smaller build on every
155
- device, so a desktop can keep the text-only file too. Once a model is requested
156
- or resolved, every status snapshot reports the build that runs in `model`, so a
157
- host that asks for `gemma4_e2b` on a phone reads `gemma4-e2b-limited` back.
158
- Hosts that need images on mobile pass `qwen3_vl_2b`; audio is desktop only. The
159
- GPU engine is far faster on a desktop, so check the browser before blaming the
160
- package:
152
+ A request without `model` picks `gemma4_e2b_limited` when the browser exposes a
153
+ WebGPU adapter and `qwen3_vl_2b` when it does not. The Gemma build is the 2.0 GB
154
+ text-only `gemma-4-E2B-it-gpu.litertlm`, and it is the only WebGPU model in the
155
+ catalog, so it loads on a desktop and on a phone alike. Every status snapshot
156
+ reports the build that runs in `model`. Hosts that need images pass
157
+ `qwen3_vl_2b`. The GPU engine is far faster on a desktop, so check the browser
158
+ before blaming the package:
161
159
 
162
160
  ```js
163
161
  // Run in the console of the page that loads the package
@@ -210,19 +208,21 @@ CPU, not the GPU. Remove it when measuring speed. Headless Chromium hides
210
208
  `--enable-features=Vulkan,WebGPU` and `--use-angle=vulkan`; check the adapter
211
209
  there too, because a missing adapter silently selects the CPU model.
212
210
 
213
- ### GPU memory, images and audio
211
+ ### GPU memory
214
212
 
215
- The WebGPU engine keeps the model, its KV cache and the vision encoder in GPU
216
- memory. The vision encoder loads on the first image turn. The audio encoder
217
- runs on the CPU backend the Gemma 4 build requires and loads on the first audio
218
- turn. A card that is already busy can lose the device (`VK_ERROR_DEVICE_LOST`)
219
- on the first image turn. The package requests the smallest Gemma 4 vision
220
- budget, 70 tokens and 630 patches, so the encoder runs one small signature and
221
- the crop is resized to 384 x 384 rather than the full 768 x 768. If the error
222
- still appears, close other GPU tabs, unload the model, or switch to the CPU
223
- model. A lost device needs a page or model reload; later calls cannot reuse it.
224
- A phone or tablet resolves Gemma to the text-only browser build, so it never
225
- starts the vision or audio encoder.
213
+ The WebGPU engine keeps the model and its KV cache in GPU memory. The Gemma
214
+ build carries no vision or audio encoder, so it never starts one and never
215
+ resizes an image. A card that is already busy can lose the device
216
+ (`VK_ERROR_DEVICE_LOST`) during a long generation. If the error appears, close
217
+ other GPU tabs, unload the model, or switch to the CPU model. A lost device
218
+ needs a page or model reload; later calls cannot reuse it.
219
+
220
+ While a reply streams, the GPU engine rests after every token readback, so the
221
+ card stays near 70% of wall time instead of pegging at 100% and starving the
222
+ desktop compositor. The rest applies after the prefill, so the first token is
223
+ not delayed. Unloading releases the WebGPU device and resets the LiteRT wasm,
224
+ because a deleted engine leaves both unusable; a later load rebuilds them from
225
+ the stored model file without downloading it again.
226
226
 
227
227
  ## Public API
228
228
 
@@ -232,16 +232,17 @@ preloadModel(input?: ChatPreloadInput): Promise<boolean>
232
232
  describeImages(input: DescribeImagesInput): Promise<ImageDescription[]>
233
233
  describeTranslated(input: DescribeTranslatedInput): Promise<string[]>
234
234
  describeModelId: ChatModelId
235
+ imageReplyTokens: number
236
+ gpuAdapter(): Promise<boolean>
235
237
  clearModelCache(): Promise<void>
236
238
  ```
237
239
 
238
240
  Call it once per response. A new call aborts the previous stream. When the
239
- requested model is already loaded, no download or load happens. On a desktop a
240
- loaded model stays resident, one per backend, so the CPU describer and the GPU
241
- translator never reload each other; a new model of the same backend replaces the
242
- old one. A phone keeps one model in total. Files stay on disk, and
243
- `clearModelCache` releases every loaded model and deletes every cached model
244
- file from this device.
241
+ requested model is already loaded, no download or load happens. One model stays
242
+ resident on every device, so a new model replaces the old one, including a move
243
+ between the CPU describer and the GPU translator. Files stay on disk, and
244
+ `clearModelCache` releases the loaded model and deletes every cached model file
245
+ from this device.
245
246
 
246
247
  `ChatInput`:
247
248
 
@@ -258,10 +259,11 @@ file from this device.
258
259
  | `onStatus?` | Receives a `ChatStatusSnapshot` on every state change. |
259
260
 
260
261
  Without `model` the package picks the GPU build when the browser provides a
261
- WebGPU adapter and the CPU build when it does not. The budget defaults are 250
262
- reply tokens, 128 CPU thinking tokens, 1200 GPU reasoning tokens (320 on the
263
- CPU), 2 images, 1 audio clip, 1000 characters per message, 10000 prompt
264
- characters and 32 CPU threads.
262
+ WebGPU adapter and the CPU build when it does not. `gpuAdapter` runs the same
263
+ probe, so a host can check the device before it names a model. The budget
264
+ defaults are 250 reply tokens, 128 CPU thinking tokens, 1200 GPU reasoning
265
+ tokens (320 on the CPU), 2 images, 1 audio clip, 1000 characters per message,
266
+ 10000 prompt characters and 32 CPU threads.
265
267
 
266
268
  `CancelChat` is `() => void`. Its meaning depends on the current state:
267
269
 
@@ -271,10 +273,9 @@ characters and 32 CPU threads.
271
273
  | Ready | Aborts the reply and unloads the model that answered. |
272
274
  | An older call | Nothing, because a newer call owns the runtime. |
273
275
 
274
- The model stays loaded between calls until a cancel frees it, and a newer
275
- model of the same backend replaces it. One model download serves every caller:
276
- a chat that joins a preload shares its progress and never starts a second
277
- download.
276
+ The model stays loaded between calls until a cancel frees it, and a newer model
277
+ replaces it. One model download serves every caller: a chat that joins a preload
278
+ shares its progress and never starts a second download.
278
279
 
279
280
  ### Preload
280
281
 
@@ -289,9 +290,8 @@ runtime, and either call joins a load already in flight. A preload owns no cance
289
290
  function, so a chat that joins its download and is cancelled leaves the download
290
291
  running.
291
292
 
292
- Preloads on different backends run together, so a host may warm the GPU text
293
- model and the CPU media model inside one `Promise.all`. Each call reports only
294
- its own model, and a load on the same backend still replaces the one in flight.
293
+ A preload of another model replaces the resident one, because one model stays
294
+ in memory at a time. A load of the same model still joins the one in flight.
295
295
 
296
296
  `ChatPreloadInput` is the subset of `ChatInput` that a preload needs:
297
297
 
@@ -312,21 +312,23 @@ on failure. A preload owns no cancel function; a new load replaces it.
312
312
  describeImages(input: DescribeImagesInput): Promise<ImageDescription[]>
313
313
  ```
314
314
 
315
- Describe every image in English with `qwen3_vl_2b_instruct_q3`, the Q3_K_M
315
+ Describe every image in English with `qwen3_vl_2b_instruct`, the Q4_K_M
316
316
  build of Unsloth Qwen3-VL 2B Instruct, one image at a time. When a WebGPU
317
317
  adapter exists and `targetLanguage` is not English, the English descriptions
318
318
  are then translated one by one with `gemma4_e2b_limited`. Without a GPU, when
319
319
  the translation model cannot load, or when the translator replies
320
320
  `CANNOT_TRANSLATE`, `description` stays equal to `descriptionEN`. Both the
321
- description and translation calls use a 600-token reply budget unless
322
- `limits.maxReplyTokens` overrides it. The describer sets an explicit 16 to
323
- 128 vision-token window, the same target the package resizes images to; other
324
- models leave the engine image limits unset.
325
-
326
- The describer and translator each receive a system message that puts accuracy
327
- first: the describer reports only visible detail, and the translator translates
328
- its own previous reply, never the user's instruction, preserving meaning, names
329
- and numbers without adding or omitting anything.
321
+ description and translation calls use the `imageReplyTokens` budget of 128
322
+ tokens unless `limits.maxReplyTokens` overrides it. The describer requests
323
+ 192 vision tokens for its image resize; the CPU engine keeps its instruct
324
+ window at 16 to 128 tokens, and other models leave the engine image limits
325
+ unset.
326
+
327
+ The describer receives a system message that asks for every element of the
328
+ image and adds rules against lying, verdicts and vague referrals.
329
+ `DescribeImagesInput.additionalRules` appends host rules to that message. The
330
+ translator translates its own previous reply, never the user's instruction,
331
+ preserving meaning, names and numbers without adding or omitting anything.
330
332
 
331
333
  `DescribeImagesInput`:
332
334
 
@@ -421,7 +423,7 @@ search and currency tools fetch over the network; `calculate` does not.
421
423
 
422
424
  | Field | Description |
423
425
  | ----- | ----------- |
424
- | `model?` | The build that runs after the mobile swap. |
426
+ | `model?` | The model that runs, or the requested build before it resolves. |
425
427
  | `availability` | `ChatStatus.online`, `.away` or `.offline`, derived from `status`. |
426
428
  | `status` | `LlmStatus`: `idle`, `downloading`, `ready`, `error` or `unsupported`. |
427
429
  | `backend` | `LlmBackend.cpu`, `.gpu` or `.none`. |
@@ -436,7 +438,7 @@ search and currency tools fetch over the network; `calculate` does not.
436
438
 
437
439
  The first snapshot fires synchronously inside `createChat`. A final snapshot
438
440
  with `requesting: false` marks the end of the call. `describeImages` reports the
439
- same snapshot for each model it loads: `qwen3_vl_2b_instruct_q3` while it
441
+ same snapshot for each model it loads: `qwen3_vl_2b_instruct` while it
440
442
  describes, then `gemma4_e2b_limited` while it translates.
441
443
 
442
444
  A stream that never produces its first chunk ends with an error after 5
@@ -461,43 +463,39 @@ without it goes quiet again.
461
463
  | `qwen3_vl_2b` | Qwen3-VL 2B | Yes | Yes | No | Yes | No |
462
464
  | `qwen3_vl_2b_instruct` | Qwen3-VL 2B Instruct | No | Yes | No | Yes | No |
463
465
  | `qwen3_vl_2b_instruct_q3` | Qwen3-VL 2B Instruct Q3 | No | Yes | No | Yes | No |
464
- | `gemma4_e2b` | Gemma 4 E2B | Yes | Yes | Yes | No | Yes |
465
466
  | `gemma4_e2b_limited` | Gemma 4 E2B limited | Yes | No | No | No | Yes |
466
467
 
467
- A phone or tablet asking for `gemma4_e2b` gets `gemma4_e2b_limited`,
468
- `gemma-4-E2B-it-gpu.litertlm`, which is text only. An explicit
469
- `gemma4_e2b_limited` loads that same file on a desktop. The build that actually
470
- runs is always the one in `ChatStatusSnapshot.model`. The limited build has no
468
+ `gemma4_e2b_limited` loads `gemma-4-E2B-it-gpu.litertlm` on every device. The
469
+ build that runs is always the one in `ChatStatusSnapshot.model`. It has no
471
470
  vision or audio encoder, so `supportsImages` and `supportsAudio` report `false`
472
- there. Use `qwen3_vl_2b` for images on mobile.
471
+ there. Use `qwen3_vl_2b` for images.
473
472
 
474
473
  The first call downloads the model before its engine loads it. Later calls
475
- reuse the loaded runtime. A desktop keeps one model per backend in memory, so
476
- the CPU describer and the GPU text model stay resident together. An interrupted
477
- download keeps every part it committed and resumes from the last one on the
478
- next attempt, including after a reload.
474
+ reuse the loaded runtime. One model stays resident, and a new model replaces it.
475
+ An interrupted download keeps every part it committed and resumes from the last
476
+ one on the next attempt, including after a reload.
479
477
 
480
478
  Qwen3-VL 2B downloads a vision projector of about 445 MB with the model, so its
481
479
  total download is about 1.5 GB. It runs on the CPU engine, with a 4096 token
482
480
  context on a phone and 8192 on a desktop. Its projector uses the same CPU thread
483
- budget as the text pass, so image turns are slower than the Gemma 4 WebGPU path.
484
- Use the GPU model when images need to be fast.
481
+ budget as the text pass, so image turns wait behind the CPU queue.
485
482
 
486
- `qwen3_vl_2b_instruct_q3` is the Unsloth Qwen3-VL 2B Instruct build that
487
- `describeImages` loads for the English pass, its Q3_K_M file. Both instruct
483
+ `qwen3_vl_2b_instruct` is the Unsloth Qwen3-VL 2B Instruct build that
484
+ `describeImages` loads for the English pass, its Q4_K_M file. Both instruct
488
485
  builds use Qwen's Q8_0 projector, about 445 MB, so the describer totals about
489
- 1.4 GB on the CPU engine. It has no reasoning pass, so it answers an image
490
- prompt directly. `qwen3_vl_2b_instruct` is the same model in Q4_K_M, about
491
- 200 MB larger, for a host that prefers the higher precision describer and
492
- names it explicitly.
493
-
494
- Gemma 4 E2B downloads `gemma-4-E2B-it.litertlm`, about 2.6 GB, on a desktop. It
495
- runs on the WebGPU engine with its vision and audio encoders in the same file,
496
- so an image or audio prompt does not download a projector. `gemma4_e2b_limited`
497
- instead downloads the 2.0 GB `gemma-4-E2B-it-gpu.litertlm`, the precompiled
498
- GPU_ARTISAN build Google ships for browsers. The smaller file is what keeps a
499
- mobile tab alive at the end of the model load, and a desktop can ask for it
500
- too, trading vision and audio for 600 MB less download.
486
+ 1.5 GB on the CPU engine. It has no reasoning pass, so it answers an image
487
+ prompt directly. `qwen3_vl_2b_instruct_q3` is the same model in Q3_K_M, about
488
+ 160 MB smaller, for a host that prefers the smaller file and names it
489
+ explicitly.
490
+
491
+ Gemma 4 E2B downloads the 2.0 GB `gemma-4-E2B-it-gpu.litertlm`, the
492
+ precompiled GPU_ARTISAN build Google ships for browsers. It runs on the WebGPU
493
+ engine, carries no vision or audio encoder, and needs no projector download.
494
+
495
+ The WebGPU engine asks for a 32768 token context on a desktop and 16384 on a
496
+ phone, both multiples of 1024. Its int8 KV cache holds the full desktop window
497
+ in about 300 MB, so the larger window stays a small part of the 2 GB model
498
+ footprint.
501
499
 
502
500
  The WebGPU engine keeps one LiteRT conversation for consecutive turns. When the
503
501
  next prompt continues that history, only the new user turn is sent. A changed
@@ -529,11 +527,9 @@ user: {next turn}
529
527
  (default 2). Older image parts are dropped and their text stays.
530
528
  - Every image part keeps its full frame and targets 128 vision tokens, with at
531
529
  most 136. Both sides are snapped to multiples of 32 px and the remainder is
532
- cropped. The WebGPU engine instead center-crops to a 384 x 384 square, the
533
- smallest Gemma 4 encoder signature.
530
+ cropped.
534
531
  - Audio is collected from the newest turns first and capped by `maxAudio`
535
- (default 1). Older audio parts are dropped and their text stays. WAV, MP3 and
536
- FLAC are accepted, and a short 16 kHz mono WAV is the best input.
532
+ (default 1). Older audio parts are dropped and their text stays.
537
533
  - Image and audio payloads do not count against `promptCharBudget`.
538
534
  - An image-only turn with a text model reports `Model does not accept images`,
539
535
  and an audio-only turn reports `Model does not accept audio`.
@@ -596,11 +592,9 @@ self hosted. Chromium also accepts
596
592
  changed. A page inside an iframe is isolated only when every ancestor sets the
597
593
  same headers.
598
594
 
599
- Each engine keeps its own cache. A desktop keeps one loaded model per backend,
600
- so a switch between the CPU and GPU models skips both the download and the
601
- load; a model of the same backend replaces the previous one. A phone keeps one
602
- model in total. The previous model's files always stay, so a switch back skips
603
- the download.
595
+ Each engine keeps its own cache. One model stays loaded, so a switch between
596
+ the CPU and GPU models skips the download but still loads the new engine. The
597
+ previous model's files always stay, so a switch back skips the download.
604
598
  `clearModelCache()` deletes them all and frees the storage. Both engines store
605
599
  model files in the origin private file system as numbered parts, under
606
600
  `wllama-store` for the CPU engine and `litert-store` for the GPU engine. The GPU
@@ -628,7 +622,7 @@ wasm under `dist/assets/litert`; model weights remain external.
628
622
 
629
623
  LiteRT-LM needs WebGPU and the JSPI and relaxed SIMD wasm features, currently
630
624
  available in Chromium. It loads a 22.7 MB wasm binary from `dist/assets/litert`
631
- and the 2.6 GB model file. The GPU engine reports progress while the file
625
+ and the 2.0 GB model file. The GPU engine reports progress while the file
632
626
  downloads and 100% of the load phase while the model compiles.
633
627
 
634
628
  wllama needs the compat worker on the same origin. When the app serves the
@@ -712,6 +706,34 @@ const useReply = () => {
712
706
  The public enums are `ChatModelId`, `LlmRole`, `ChatStatus`, `LlmStatus`,
713
707
  `LlmBackend` and `LlmPhase`.
714
708
 
709
+ ## Testing
710
+
711
+ The suites live in `test/` and run against the local checkout.
712
+
713
+ `yarn test:instruct` starts a local llama.cpp server with the Qwen3-VL 2B
714
+ Instruct Q4_K_M build:
715
+
716
+ ```bash
717
+ yarn test:instruct
718
+ ```
719
+
720
+ It runs `llama serve` on `http://127.0.0.1:8080` with a 160k context, 4
721
+ threads and CPU inference. llama.cpp
722
+ downloads the weights on first run and caches them outside the repository.
723
+ llama.cpp runs about 10x faster than wllama, so prefer the live server
724
+ whenever a test needs real inference. Keep the server in its own terminal
725
+ while the live and image suites run.
726
+
727
+ The image suite bundles the browser harness, serves it from `test/drive.cjs`
728
+ and proxies `/llama/` to the server on port 8080, so start `test:instruct`
729
+ first:
730
+
731
+ ```bash
732
+ yarn test:images
733
+ ```
734
+
735
+ The fake-engine harness needs no server and never reaches the network.
736
+
715
737
  ## Architecture
716
738
 
717
739
  An interactive map of the package lives at
@@ -719,6 +741,9 @@ An interactive map of the package lives at
719
741
  the two entries, every source module with line numbers, the helper edges and the
720
742
  send flow.
721
743
 
744
+ A side-by-side comparison with `@mlc-ai/web-llm` for Gemma 4 E2B on WebGPU lives
745
+ at [degreesign.github.io/ds_chat/vs_web_llm.html](https://degreesign.github.io/ds_chat/vs_web_llm.html).
746
+
722
747
  ## FAQ
723
748
 
724
749
  ### What is @degreesign/chat?
@@ -733,25 +758,19 @@ No. The host passes the full prompt on every call and keeps the reply.
733
758
  ### Does it support images?
734
759
 
735
760
  Yes, on models with vision support. Put image URLs in a user turn's `images`
736
- array. Qwen3-VL 2B accepts images on the CPU engine and Gemma 4 E2B accepts
737
- images on the desktop WebGPU engine. On a phone or tablet Gemma resolves to
738
- `gemma4_e2b_limited`, the text-only browser build, so use `qwen3_vl_2b` for
739
- mobile images. Each image keeps its full frame and targets 128 vision tokens,
740
- at most 136, with both sides snapped to multiples of 32 px and the remainder
741
- cropped. The WebGPU engine instead center-crops a 384 x 384 square with the
742
- smallest Gemma 4 vision budget. A model without vision drops images
761
+ array. Qwen3-VL 2B accepts images on the CPU engine and is the vision model in
762
+ the catalog. Each image keeps its full frame and targets 128 vision tokens, at
763
+ most 136, with both sides snapped to multiples of 32 px and the remainder
764
+ cropped. The WebGPU build is text only, so a model without vision drops images
743
765
  when the turn also has text; an image-only turn reports
744
766
  `Model does not accept images`.
745
767
 
746
768
  ### Does it support audio?
747
769
 
748
- Yes, on models with audio support. Put an audio URL in a user turn's `audio`
749
- array. Gemma 4 E2B accepts WAV, MP3 and FLAC on the desktop WebGPU engine, and a
750
- short 16 kHz mono WAV is the best input. `limits.maxAudio` caps the clips
751
- (default 1). Audio is desktop only; `gemma4_e2b_limited` has no audio encoder.
752
- The package does not transcode, so the host supplies one of those formats. A
753
- model without audio drops the clip when the turn also has text; an audio-only
754
- turn reports `Model does not accept audio`.
770
+ No model in the catalog has an audio encoder. A user turn can still carry an
771
+ audio URL in `audio`, and `limits.maxAudio` caps the clips (default 1), but a
772
+ model without audio support drops the clip when the turn also has text, and an
773
+ audio-only turn reports `Model does not accept audio`.
755
774
 
756
775
  ### Does it need a server?
757
776
 
@@ -774,11 +793,11 @@ when an older version stored the file. The package stores no model data itself.
774
793
 
775
794
  ### Which model should I pick?
776
795
 
777
- Qwen3-VL 2B supports reasoning and images on the CPU. Gemma 4 E2B supports
778
- reasoning, images and audio on desktop WebGPU, with much faster media prompts
779
- but a larger download and a slower first compile. Use Qwen for broad browser
780
- support and for mobile images. Use Gemma on a desktop with Chromium WebGPU, or
781
- on a phone for text, where it resolves to `gemma4_e2b_limited`.
796
+ Qwen3-VL 2B supports reasoning and images on the CPU, and it runs on any browser
797
+ with WebAssembly. Gemma 4 E2B limited supports reasoning on WebGPU, with faster
798
+ text generation but a larger download and a slower first compile. Use Qwen for
799
+ broad browser support and for images. Use Gemma on a device with a WebGPU
800
+ adapter when text speed matters.
782
801
 
783
802
  ## Licensing
784
803