@tanstack-ai-browser/wllama 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +151 -2
- package/dist/adapter.d.ts +166 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +183 -0
- package/dist/audio.d.ts +16 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +80 -0
- package/dist/catalog.d.ts +104 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +307 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +148 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/model-memory.d.ts +46 -0
- package/dist/model-memory.d.ts.map +1 -0
- package/dist/model-memory.js +79 -0
- package/dist/model-meta.d.ts +7 -0
- package/dist/model-meta.d.ts.map +1 -0
- package/dist/model-meta.js +5 -0
- package/dist/shared-load.d.ts +48 -0
- package/dist/shared-load.d.ts.map +1 -0
- package/dist/shared-load.js +103 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +17 -0
- package/dist/template.d.ts +26 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +25 -0
- package/dist/thinking.d.ts +39 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +140 -0
- package/dist/tool-calls.d.ts +33 -0
- package/dist/tool-calls.d.ts.map +1 -0
- package/dist/tool-calls.js +203 -0
- package/package.json +62 -3
- package/src/adapter.ts +330 -0
- package/src/audio.test.ts +83 -0
- package/src/audio.ts +79 -0
- package/src/catalog.ts +399 -0
- package/src/client.ts +195 -0
- package/src/index.ts +11 -0
- package/src/runtime.test.ts +220 -0
- package/src/wllama-version.test.ts +19 -0
- package/src/wrapper.test.ts +173 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 daveycodez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tanstack-ai-browser/wllama
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [TanStack AI](https://tanstack.com/ai) adapter for GGUF models that run in the browser, with [wllama](https://github.com/ngxson/wllama): llama.cpp compiled to WebAssembly, in a Web Worker. It runs on WebGPU where the browser has it, and on the CPU elsewhere, so it runs in any browser with WebAssembly SIMD: Firefox on Android and iPhone Safari included. It runs the official [`@wllama/wllama`](https://www.npmjs.com/package/@wllama/wllama) package (3.13.0) as published, through its public API and options, with its own WebAssembly builds: no patched, vendored or rebuilt copy of wllama or llama.cpp. A wllama or llama.cpp bug is fixed here when a wllama release with the fix is out.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @tanstack-ai-browser/wllama @tanstack/ai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { chat } from "@tanstack/ai"
|
|
11
|
+
import { wllamaText } from "@tanstack-ai-browser/wllama"
|
|
12
|
+
|
|
13
|
+
const stream = chat({
|
|
14
|
+
adapter: wllamaText("LiquidAI/LFM2.5-1.2B-Instruct-GGUF"),
|
|
15
|
+
messages,
|
|
16
|
+
threadId
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
- **Any bundler:** install and go. wllama's WebAssembly file loads from jsDelivr by default, at the installed wllama's version, and Safari's compat build too (wllama's own default); nothing is emitted into your build. To host them yourself, see [Self-hosting the WebAssembly](#self-hosting-the-webassembly).
|
|
23
|
+
- **Storage:** wllama downloads models to the browser's Origin Private File System. Nothing to set up.
|
|
24
|
+
- **Threads (optional):** serve the site cross-origin isolated (`Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`) and wllama runs on half the cores; otherwise on one. Set the headers on your host, and in development (below). With `require-corp`, cross-origin images and other resources load only with CORS (`<img crossOrigin="anonymous">`) or a `Cross-Origin-Resource-Policy` header (jsDelivr sends both).
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
const headers = {
|
|
29
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
30
|
+
"Cross-Origin-Embedder-Policy": "require-corp"
|
|
31
|
+
}
|
|
32
|
+
export default defineConfig({ server: { headers }, preview: { headers } })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// next.config.ts
|
|
37
|
+
const nextConfig: NextConfig = {
|
|
38
|
+
async headers() {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
source: "/(.*)",
|
|
42
|
+
headers: [
|
|
43
|
+
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
44
|
+
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Exports
|
|
53
|
+
|
|
54
|
+
The package is shaped like `@tanstack/ai-openai`'s text adapter:
|
|
55
|
+
|
|
56
|
+
- `@tanstack-ai-browser/wllama`: `wllamaText(model, config?)`, `WllamaTextAdapter` (with the runtime methods `availability`, `create` and `destroy`) and the types `WllamaConfig`, `WllamaModel`, `WllamaTextModelOptions`, `Availability` and `DownloadProgressCallback`.
|
|
57
|
+
- The catalog, from the same root: `WLLAMA_CHAT_MODELS`, `WLLAMA_MODEL_META` and `WLLAMA_MODEL_REASONING`, with the types `WllamaChatModel`, `WllamaModelMeta` and `WllamaModelReasoningByName`.
|
|
58
|
+
|
|
59
|
+
## Models
|
|
60
|
+
|
|
61
|
+
`WLLAMA_CHAT_MODELS` lists the models this package knows, and `WLLAMA_MODEL_META` has each one's details: Hugging Face repository, GGUF file, capabilities, reasoning levels and the sampling from its card. They're Liquid AI's official GGUF builds, and Unsloth's build of Qwen3.5 2B.
|
|
62
|
+
|
|
63
|
+
Any other GGUF works too: name its file, as `"owner/repo/file.gguf"` on Hugging Face (its latest version) or as a URL. An unlisted model gets llama.cpp's default sampling and the chat template's `enable_thinking` switch for `chat({ reasoning })`.
|
|
64
|
+
|
|
65
|
+
| Model | Reasoning | File |
|
|
66
|
+
| ------------------------------------ | ------------------------------- | --------------------------------------- |
|
|
67
|
+
| `LiquidAI/LFM3-1.7B-GGUF` | on or off (`"high"` or `"off"`) | Q4_K_M, 1.10 GB |
|
|
68
|
+
| `LiquidAI/LFM3-VL-2B-GGUF` | on or off (`"high"` or `"off"`) | Q4_K_M, 1.10 GB, and a 203 MB projector |
|
|
69
|
+
| `LiquidAI/LFM2.5-VL-3B-GGUF` | none | Q4_K_M, 1.67 GB, and a 583 MB projector |
|
|
70
|
+
| `LiquidAI/LFM2.5-VL-1.6B-GGUF` | none | Q4_K_M, 731 MB, and a 583 MB projector |
|
|
71
|
+
| `LiquidAI/LFM2.5-VL-450M-GGUF` | none | Q4_K_M, 229 MB, and a 103 MB projector |
|
|
72
|
+
| `LiquidAI/LFM3-Omni-2B-GGUF` | none | Q4_K_M, 1.10 GB, and a 507 MB projector |
|
|
73
|
+
| `LiquidAI/LFM3-Audio-2B-GGUF` | none | Q4_K_M, 1.10 GB, and a 304 MB projector |
|
|
74
|
+
| `LiquidAI/LFM2.5-Audio-1.5B-GGUF` | none | Q4_0, 696 MB, and a 220 MB projector |
|
|
75
|
+
| `LiquidAI/LFM2.5-1.2B-Instruct-GGUF` | none | Q4_K_M, 731 MB |
|
|
76
|
+
| `LiquidAI/LFM2.5-1.2B-Thinking-GGUF` | always (`"high"`) | Q4_K_M, 731 MB |
|
|
77
|
+
| `LiquidAI/LFM2.5-350M-GGUF` | none | Q4_K_M, 229 MB |
|
|
78
|
+
| `unsloth/Qwen3.5-2B-GGUF` | on or off (`"high"` or `"off"`) | Q4_K_M, 1.28 GB |
|
|
79
|
+
|
|
80
|
+
Liquid's models are Q4_K_M: on a Pixel 10 Pro in Firefox, LFM2.5 350M read prompts at 55 tokens/s in Q4_K_M, 40 in Q8_0 and 15 in Q4_0 with wllama's own build. In Chrome on an M3 Ultra, Qwen3.5 2B on WebGPU read about 850 prompt tokens/s and wrote about 60 tokens/s (with other work on the machine).
|
|
81
|
+
|
|
82
|
+
wllama answers llama.cpp's OpenAI-compatible Chat Completions API in the page, so the adapter is TanStack's Chat Completions adapter (`@tanstack/openai-base`) with the loaded model as its client: messages, tools, structured output, the stream and usage are TanStack's and llama.cpp's. It adds loading the model, the chat template's thinking switch and llama.cpp's option names.
|
|
83
|
+
|
|
84
|
+
**Tools** work as with any adapter: pass them to `chat({ tools })`, MCP tools included. llama.cpp writes them into the prompt with the model's chat template and parses the model's calls back.
|
|
85
|
+
|
|
86
|
+
LFM3 needs wllama 3.13 or later: its llama.cpp is the first published with LFM3's architecture.
|
|
87
|
+
|
|
88
|
+
**Images:** Liquid's vision models (LFM3-VL 2B and LFM2.5-VL 3B, 1.6B and 450M) read images: send image parts in a user message, as data (base64) or URLs the page can fetch. Each loads with its vision projector, a second GGUF file in the same repository (the Q8_0 one), and the adapter's `inputModalities` has images for them. Another GGUF loads as text only.
|
|
89
|
+
|
|
90
|
+
**Audio:** LFM3-Omni 2B (which reads images too), LFM3-Audio 2B and LFM2.5-Audio 1.5B read audio: send audio parts in a user message, as data (base64) or URLs the page can fetch, and the adapter's `inputModalities` has audio for them. They transcribe a recording (when asked, or with the system prompt `Perform ASR.`) and answer questions about it, in text: their speech output isn't used. llama.cpp decodes WAV, MP3 and FLAC itself; other audio (Opus in WebM or Ogg, AAC in MP4) is decoded in the page with Web Audio and goes as WAV, so it needs a browser. Each loads with its projector, the audio encoder (Q8_0, or Q4_0 for LFM2.5-Audio, whose repository has no Q4_K_M model either). The LFM3 ones call tools; LFM2.5-Audio doesn't, and told of tools it answers that none can hear a recording, so send it none. LFM3-Audio reasons when it chooses to: its chat template has no switch.
|
|
91
|
+
|
|
92
|
+
**Reasoning:** thinking (llama.cpp's `reasoning_content`) streams as reasoning events. LFM3 thinks by default; `chat({ reasoning: "off" })` turns it off, through its chat template's `enable_thinking` (llama.cpp's `chat_template_kwargs`). Qwen3.5 2B doesn't think unless asked; `chat({ reasoning: "high" })` turns it on the same way. LFM2.5 1.2B Thinking always thinks. `chat({ reasoning })` is a type error for the models that don't reason.
|
|
93
|
+
|
|
94
|
+
**Options** go in `modelOptions` per call: llama.cpp's own request fields, sent as given, as TanStack's adapters do with their provider's fields. wllama answers with llama.cpp's OpenAI-compatible server code, so these are OpenAI's names and llama.cpp's: `max_tokens`, `temperature`, `top_p`, `top_k`, `min_p`, `repeat_penalty`, `frequency_penalty`, `presence_penalty`, `seed`, `stop`, `logit_bias`, `parallel_tool_calls` (false constrains the model to one tool call; left out, llama.cpp allows several where the chat template supports them) and `chat_template_kwargs`; llama.cpp's other fields go through too. Sampling left out comes from the model card (`WLLAMA_MODEL_META[model].sampling`, in the same names). Without `max_tokens`, a reply may run to the context limit, as llama.cpp's default.
|
|
95
|
+
|
|
96
|
+
**Structured output:** `chat({ outputSchema })` sends the schema as given in `response_format`, and llama.cpp constrains the answer to it with its JSON Schema grammar. With `stream: true` it streams as TanStack's standard events.
|
|
97
|
+
|
|
98
|
+
**Context:** `wllamaText(model, { contextLength })` sets how many tokens a chat may hold, prompt and reply together. By default it's the model's whole context window, read from its GGUF (262,144 for LFM3 and Qwen3.5 2B, 128,000 for LFM2.5). Unlike Transformers.js and the WebGL engine, which grow their attention memory as a chat fills it, llama.cpp reserves it for the whole context when the model loads, which can be GBs for a full window. Fine on a desktop; on an iPhone set 8,192. On the CPU, where that memory is WebAssembly's (4 GB at most), set it too: a model whose whole window doesn't fit there (Qwen3.5 2B's 262,144 tokens take 3 GB) fails to load with an error. Changing it loads the model again.
|
|
99
|
+
|
|
100
|
+
**Usage** is in each call's `RUN_FINISHED` event, as llama.cpp reports it: `promptTokens`, `completionTokens`, `totalTokens`, and `promptTokensDetails.cachedTokens` for the start of the prompt llama.cpp reused from an earlier call. Timings aren't in it: measure them from the stream, as for any adapter.
|
|
101
|
+
|
|
102
|
+
**Prompt reuse:** use the standard TanStack AI option, for example
|
|
103
|
+
`chat({ adapter, messages, threadId, promptCache: "none" })` or
|
|
104
|
+
`promptCache: { retention: "short", key: "conversation-key" }`. No adapter-specific
|
|
105
|
+
cache option is needed. Structured output passes its exact schema through native
|
|
106
|
+
`response_format.json_schema`, separately from the conversation; schemas are not
|
|
107
|
+
inserted into caller/system messages.
|
|
108
|
+
|
|
109
|
+
`"none"` maps internally to llama.cpp's public `cache_prompt: false`. Omitted, `"short"`,
|
|
110
|
+
and `"long"` policies preserve native runtime defaults and introduce no TTL. The standard
|
|
111
|
+
key has no advertised native slot/session equivalent, so the adapter does not invent one.
|
|
112
|
+
Actual reuse comes from llama.cpp's reported `cached_tokens`, forwarded as
|
|
113
|
+
`promptTokensDetails.cachedTokens`; enabling caching alone does not prove a hit.
|
|
114
|
+
|
|
115
|
+
Reuse lasts as long as the loaded model: llama.cpp keeps what it has read in memory, and
|
|
116
|
+
a reload of the page or the model reads the prompt again.
|
|
117
|
+
|
|
118
|
+
**Downloads:** each adapter has the runtime methods of this project's adapters, with the Prompt API's values:
|
|
119
|
+
|
|
120
|
+
- `adapter.availability()` resolves to `"unavailable"` where wllama can't run (it needs WebAssembly, Web Workers and the Origin Private File System; not on a server), `"downloading"` while this page downloads the model, `"available"` when the model is loaded or its whole GGUF file is in wllama's storage, else `"downloadable"`. It lists wllama's storage only (its `ModelManager`, which reads the Origin Private File System): no worker, no WebAssembly, no network.
|
|
121
|
+
- `adapter.create({ onDownloadProgress?, signal }?)` downloads and loads the model ahead of time, with the download's progress, 0 to 1, then 1 once it's ready (at once if it already is). Aborting `signal` stops that call's wait (it rejects at once); the load goes on, and `destroy()` stops it. It resolves to the adapter, and rejects on failure; it's fine to leave unawaited. The config's `onDownloadProgress` hears a download the first `chat()` starts too.
|
|
122
|
+
- `adapter.destroy()` unloads the model (stopping its download), if it's the one loaded; its file stays in storage.
|
|
123
|
+
|
|
124
|
+
The page holds one wllama model at a time, loaded with the settings of the adapter that asked for it (`contextLength`, `gpu`, `threads`, the WebAssembly URLs): adapters with the same settings share it, another adapter's settings load it again, and `destroy()` on one unloads it for all of them.
|
|
125
|
+
|
|
126
|
+
## How it runs
|
|
127
|
+
|
|
128
|
+
`chat()` runs tools and the agent loop; each model call is one chat completion from llama.cpp's OpenAI-compatible API (`createChatCompletion`), in wllama's worker. llama.cpp writes the prompt with the model's chat template, reads thinking and tool calls back, and reuses whatever start of the prompt it has already read. wllama's defaults apply, except:
|
|
129
|
+
|
|
130
|
+
- **Context:** the model's own training context (see above), not wllama's 1,024, which a system prompt with tools outgrows.
|
|
131
|
+
- **WebGPU or CPU:** wllama's default: on WebGPU where the browser has it and wllama can drive it (Chrome and Edge with JSPI, Safari through wllama's compat build), on the CPU elsewhere (Firefox, which has no JSPI, and browsers without WebGPU). `wllamaText(model, { gpu: false })` runs on the CPU only. llama.cpp's WebGPU and CPU code are wllama 3.13.0's, unchanged.
|
|
132
|
+
- **Full sliding-window cache:** `swa_full: true`, so a model with sliding-window layers reuses what it has read after a tool call instead of rereading the whole prompt. It costs a little memory, and nothing for models without such layers.
|
|
133
|
+
- **Threads:** wllama's default, half the cores, where the page is cross-origin isolated (`Cross-Origin-Opener-Policy: same-origin` with `Cross-Origin-Embedder-Policy: require-corp`; Safari and Firefox on Android ignore `credentialless`); one elsewhere. `wllamaText(model, { threads })` sets a number.
|
|
134
|
+
|
|
135
|
+
The GGUF file is saved to the Origin Private File System by wllama, and loads from there afterwards, offline too.
|
|
136
|
+
|
|
137
|
+
### Self-hosting the WebAssembly
|
|
138
|
+
|
|
139
|
+
By default wllama loads its WebAssembly file from jsDelivr, at the version this package depends on (`https://cdn.jsdelivr.net/npm/@wllama/wllama@3.13.0/esm/wasm/wllama.wasm`), as wllama loads its compat build for Safari. To serve them with your site (offline use, no third-party request), copy `@wllama/wllama/esm/wasm/wllama.wasm` and, for Safari, `@wllama/wllama-compat`'s `wasm/wllama.wasm` and `wasm/wllama.js` (same version), and pass their URLs. The file must match the installed wllama's JavaScript, so copy it from the same version.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
wllamaText("LiquidAI/LFM2.5-350M-GGUF", {
|
|
143
|
+
wasmUrl: "/wllama.wasm",
|
|
144
|
+
compat: { wasm: "/wllama-compat.wasm", worker: "/wllama-compat.js" }
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`compat` goes to wllama's `setCompat`.
|
|
149
|
+
|
|
150
|
+
**Status:** early (0.x); the API may change. See the [repository README](https://github.com/daveycodez/tanstack-ai-browser#readme).
|
|
151
|
+
|
|
152
|
+
Thinking-capable models accept `reasoning: { level: "high", budgetTokens: 512 }` through TanStack AI. The adapter forwards an explicit budget through wllama's public `reasoning_budget_tokens` option, alongside the template's thinking switch. Omitting the budget preserves the runtime default; `level: "off"` ignores a valid budget on models that support switching thinking off. Zero is forwarded unchanged. Negative, fractional, non-finite, and unsafe budgets fail before inference.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TanStack AI text adapter for GGUF models that run in the browser with
|
|
3
|
+
* wllama (llama.cpp in WebAssembly). wllama answers llama.cpp's
|
|
4
|
+
* OpenAI-compatible Chat Completions API in the page, so this is TanStack's
|
|
5
|
+
* Chat Completions adapter with the loaded model as its client: messages,
|
|
6
|
+
* tools, structured output and the stream (text, thinking, tool calls) are
|
|
7
|
+
* TanStack's, and llama.cpp writes the prompt with the model's chat template
|
|
8
|
+
* and reads its thinking and tool calls back. This file adds what's wllama's
|
|
9
|
+
* own: loading the model with the adapter's settings, the chat template's
|
|
10
|
+
* thinking switch, the model card's sampling, and audio parts for the
|
|
11
|
+
* models that read them.
|
|
12
|
+
*/
|
|
13
|
+
import type { TextOptions } from "@tanstack/ai";
|
|
14
|
+
import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
|
|
15
|
+
import type { OpenAIClientLike } from "@tanstack/openai-base";
|
|
16
|
+
import type { WllamaCompat } from "@wllama/wllama/esm/index.js";
|
|
17
|
+
import type { Availability, DownloadProgressCallback } from "./client.js";
|
|
18
|
+
import type { WllamaChatModel, WllamaModel, WllamaModelReasoningByName } from "./catalog.js";
|
|
19
|
+
export type WllamaConfig = {
|
|
20
|
+
/**
|
|
21
|
+
* Most tokens a chat may hold, prompt and reply together: the model's
|
|
22
|
+
* whole context window by default (its GGUF's training context), never
|
|
23
|
+
* more. llama.cpp reserves the attention cache for all of it when the
|
|
24
|
+
* model loads (LFM3: ~12 KB a token, so 3 GB for its 262,144), so set
|
|
25
|
+
* less where memory is short: 8,192 on an iPhone. Changing it loads the
|
|
26
|
+
* model again.
|
|
27
|
+
*/
|
|
28
|
+
contextLength?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Where wllama's WebAssembly file is served. By default, the installed
|
|
31
|
+
* `@wllama/wllama`'s own file on jsDelivr, at its version; serve
|
|
32
|
+
* `@wllama/wllama/esm/wasm/wllama.wasm` with the site and pass its URL to
|
|
33
|
+
* host it yourself.
|
|
34
|
+
*/
|
|
35
|
+
wasmUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Where wllama's compat build (for Safari: `@wllama/wllama-compat`'s
|
|
38
|
+
* `wasm/wllama.wasm` and `wasm/wllama.js`) is served, passed to
|
|
39
|
+
* `Wllama.setCompat`. By default, wllama's: the same version on jsDelivr.
|
|
40
|
+
*/
|
|
41
|
+
compat?: WllamaCompat;
|
|
42
|
+
/** CPU threads. wllama's default otherwise: half the cores. */
|
|
43
|
+
threads?: number;
|
|
44
|
+
/**
|
|
45
|
+
* `false` to run on the CPU only. By default wllama runs on WebGPU where
|
|
46
|
+
* it can, and on the CPU elsewhere.
|
|
47
|
+
*/
|
|
48
|
+
gpu?: boolean;
|
|
49
|
+
/** wllama's and llama.cpp's logs, in the console. */
|
|
50
|
+
debug?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The model's download progress, 0 to 1, when a call (`chat()`, say)
|
|
53
|
+
* starts its download; then 1 once it's ready. To load it ahead of time
|
|
54
|
+
* with progress, see `create`.
|
|
55
|
+
*/
|
|
56
|
+
onDownloadProgress?: DownloadProgressCallback;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Per-call `modelOptions`: llama.cpp's own request fields, sent as given
|
|
60
|
+
* (wllama answers with llama.cpp's OpenAI-compatible server code), over the
|
|
61
|
+
* model card's sampling. These are the common ones; llama.cpp's others go
|
|
62
|
+
* through too.
|
|
63
|
+
*/
|
|
64
|
+
export type WllamaTextModelOptions = {
|
|
65
|
+
/** Most tokens to generate, thinking included. llama.cpp's default otherwise: up to the context limit. */
|
|
66
|
+
max_tokens?: number;
|
|
67
|
+
/** Randomness of sampling: 0 is greedy. */
|
|
68
|
+
temperature?: number;
|
|
69
|
+
/** Nucleus sampling: the likeliest tokens up to this probability, in (0, 1]. */
|
|
70
|
+
top_p?: number;
|
|
71
|
+
/** Sampling from the likeliest `top_k` tokens only (0: all of them). */
|
|
72
|
+
top_k?: number;
|
|
73
|
+
/** Drops tokens less likely than this share of the likeliest one's odds. */
|
|
74
|
+
min_p?: number;
|
|
75
|
+
/** Penalizes tokens already in the chat (1: off). */
|
|
76
|
+
repeat_penalty?: number;
|
|
77
|
+
/** Lowers the odds of tokens by how often they've appeared. */
|
|
78
|
+
frequency_penalty?: number;
|
|
79
|
+
/** Lowers the odds of tokens that have appeared at all. */
|
|
80
|
+
presence_penalty?: number;
|
|
81
|
+
/** Seeds the sampler, for the same samples from the same request. */
|
|
82
|
+
seed?: number;
|
|
83
|
+
/** Where to stop: the reply ends before the first of these it would write. */
|
|
84
|
+
stop?: string | Array<string>;
|
|
85
|
+
/** Adds to the odds of tokens, by token id. */
|
|
86
|
+
logit_bias?: Record<string, number>;
|
|
87
|
+
/**
|
|
88
|
+
* False constrains the model to one tool call. True or left out: as many
|
|
89
|
+
* as the model writes, when its chat template supports several.
|
|
90
|
+
*/
|
|
91
|
+
parallel_tool_calls?: boolean;
|
|
92
|
+
/** The chat template's own variables; `chat({ reasoning })` sets its `enable_thinking`. */
|
|
93
|
+
chat_template_kwargs?: Record<string, unknown>;
|
|
94
|
+
};
|
|
95
|
+
/** A model's reasoning levels: a listed model's, none for a listed one that doesn't reason, else the switch. */
|
|
96
|
+
type ResolveReasoning<TModel extends string> = TModel extends keyof WllamaModelReasoningByName ? WllamaModelReasoningByName[TModel] : TModel extends WllamaChatModel ? never : {
|
|
97
|
+
levels: "off" | "high";
|
|
98
|
+
budget: true;
|
|
99
|
+
};
|
|
100
|
+
type MetadataByModality = {
|
|
101
|
+
text: unknown;
|
|
102
|
+
image: unknown;
|
|
103
|
+
audio: unknown;
|
|
104
|
+
video: unknown;
|
|
105
|
+
document: unknown;
|
|
106
|
+
};
|
|
107
|
+
/** The base adapter's conversion of a message part to a Chat Completions one. */
|
|
108
|
+
type ConvertContentPart = OpenAIBaseChatCompletionsTextAdapter<string>["convertContentPart"];
|
|
109
|
+
/** A Chat Completions request, as the base adapter writes it. */
|
|
110
|
+
type CreateParams = Parameters<OpenAIClientLike["chat"]["completions"]["create"]>[0];
|
|
111
|
+
export declare class WllamaTextAdapter<TModel extends WllamaModel> extends OpenAIBaseChatCompletionsTextAdapter<TModel, WllamaTextModelOptions, ReadonlyArray<"text" | "image" | "audio">, MetadataByModality, ReadonlyArray<string>, ResolveReasoning<TModel>> {
|
|
112
|
+
readonly name = "wllama";
|
|
113
|
+
readonly api = "wllama";
|
|
114
|
+
/** Text, and images or audio for a listed model whose projector reads them. */
|
|
115
|
+
readonly inputModalities: ReadonlyArray<"text" | "image" | "audio">;
|
|
116
|
+
private readonly settings;
|
|
117
|
+
private readonly onDownloadProgress;
|
|
118
|
+
constructor(config: WllamaConfig, model: TModel);
|
|
119
|
+
/**
|
|
120
|
+
* Where the model stands in this browser, with the Prompt API's values:
|
|
121
|
+
* "available" once it's in wllama's storage (or loaded), "downloading"
|
|
122
|
+
* while this page downloads it, else "downloadable"; "unavailable" where
|
|
123
|
+
* wllama can't run.
|
|
124
|
+
*/
|
|
125
|
+
availability(): Promise<Availability>;
|
|
126
|
+
/**
|
|
127
|
+
* Downloads the model if it isn't stored and loads it with this adapter's
|
|
128
|
+
* settings, with the download's progress, 0 to 1, then 1 once it's ready.
|
|
129
|
+
* Resolves to this adapter; rejects if it can't load (fine to leave
|
|
130
|
+
* unawaited). `chat()` doesn't need it: its first call loads the model.
|
|
131
|
+
*/
|
|
132
|
+
create(options?: {
|
|
133
|
+
onDownloadProgress?: DownloadProgressCallback;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
}): Promise<this>;
|
|
136
|
+
/** Unloads the model, freeing its memory. It stays in wllama's storage. */
|
|
137
|
+
destroy(): Promise<void>;
|
|
138
|
+
/** The request, with `modelOptions` as given over the model card's sampling, and the chat template's thinking switch. */
|
|
139
|
+
protected mapOptionsToRequest(options: TextOptions): CreateParams & {
|
|
140
|
+
stream: true;
|
|
141
|
+
};
|
|
142
|
+
/** llama.cpp streams thinking as `reasoning_content`. */
|
|
143
|
+
protected extractReasoning(chunk: unknown): {
|
|
144
|
+
text: string;
|
|
145
|
+
} | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* An audio part by its URL, as an image goes: Chat Completions takes
|
|
148
|
+
* audio only as base64 in two formats, and the client reads the bytes of
|
|
149
|
+
* either kind from a URL (a data URL for base64).
|
|
150
|
+
*/
|
|
151
|
+
protected convertContentPart(part: Parameters<ConvertContentPart>[0]): ReturnType<ConvertContentPart>;
|
|
152
|
+
/** Schemas as given: llama.cpp's grammar takes any JSON Schema, without OpenAI's strict-mode rewrite. */
|
|
153
|
+
protected makeStructuredOutputCompatible(schema: Record<string, any>): Record<string, any>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A TanStack AI text adapter for a GGUF model that wllama runs in the
|
|
157
|
+
* browser.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* chat({ adapter: wllamaText("unsloth/Qwen3.5-2B-GGUF"), messages, threadId })
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export declare function wllamaText<TModel extends WllamaModel>(model: TModel, config?: WllamaConfig): WllamaTextAdapter<TModel>;
|
|
165
|
+
export {};
|
|
166
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { resolveReasoning } from "@tanstack/ai/adapter-internals";
|
|
2
|
+
import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
|
|
3
|
+
import { llamaAudio } from "./audio.js";
|
|
4
|
+
import { availability, loadModel, unloadModel } from "./client.js";
|
|
5
|
+
import { listed, modelMeta } from "./catalog.js";
|
|
6
|
+
/** The switch for a model the catalog doesn't list: the chat template's `enable_thinking`. */
|
|
7
|
+
const UNLISTED_REASONING = { levels: { off: "false", high: "true" }, budget: true };
|
|
8
|
+
/** A model's reasoning levels: the catalog's, or the switch for one it doesn't list. */
|
|
9
|
+
const reasoningOf = (model) => (listed(model) ? modelMeta(model).reasoning : UNLISTED_REASONING);
|
|
10
|
+
/**
|
|
11
|
+
* The chat template's `enable_thinking` for a request, through llama.cpp's
|
|
12
|
+
* `chat_template_kwargs`, and a thinking budget: left out when the model has
|
|
13
|
+
* no levels or the request doesn't ask, so the template's default applies.
|
|
14
|
+
*/
|
|
15
|
+
function thinkingKwargs(request, model, kwargs) {
|
|
16
|
+
const value = resolveReasoning(request, reasoningOf(model))?.value;
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return {};
|
|
19
|
+
const thinking = value === "true";
|
|
20
|
+
const budget = request?.budgetTokens;
|
|
21
|
+
return {
|
|
22
|
+
chat_template_kwargs: { ...kwargs, enable_thinking: thinking },
|
|
23
|
+
...(thinking && budget !== undefined && { reasoning_budget_tokens: budget })
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The card's sampling for a request: its thinking settings while the chat
|
|
28
|
+
* template thinks (asked for, or a model that always thinks), else its
|
|
29
|
+
* own. None for a model the catalog doesn't list: llama.cpp's defaults.
|
|
30
|
+
*/
|
|
31
|
+
function cardSampling(request, model) {
|
|
32
|
+
if (!listed(model))
|
|
33
|
+
return {};
|
|
34
|
+
const meta = modelMeta(model);
|
|
35
|
+
const value = resolveReasoning(request, meta.reasoning)?.value;
|
|
36
|
+
const thinking = value !== undefined ? value === "true" : !!meta.reasoning && !("off" in meta.reasoning.levels);
|
|
37
|
+
return (thinking && meta.thinkingSampling) || meta.sampling;
|
|
38
|
+
}
|
|
39
|
+
/** What a URL holds: a data URL's bytes, or those the page fetches. */
|
|
40
|
+
const bytesAt = async (url, signal) => (await fetch(url, { ...(signal && { signal }) })).arrayBuffer();
|
|
41
|
+
/**
|
|
42
|
+
* A request's messages with each image and audio clip as its bytes: wllama
|
|
43
|
+
* reads them as `{ type: "image", data }` and `{ type: "audio", data }`,
|
|
44
|
+
* where the request has an `image_url` or an `audio_url` (a data URL here,
|
|
45
|
+
* or one the page can fetch). Audio llama.cpp can't decode goes as WAV.
|
|
46
|
+
*/
|
|
47
|
+
async function withMediaBytes(messages, signal) {
|
|
48
|
+
if (!Array.isArray(messages))
|
|
49
|
+
return messages;
|
|
50
|
+
return Promise.all(messages.map(async (message) => {
|
|
51
|
+
if (!Array.isArray(message.content))
|
|
52
|
+
return message;
|
|
53
|
+
const content = await Promise.all(message.content.map(async (part) => {
|
|
54
|
+
if (part.type === "image_url" && part.image_url) {
|
|
55
|
+
return { type: "image", data: await bytesAt(part.image_url.url, signal) };
|
|
56
|
+
}
|
|
57
|
+
if (part.type === "audio_url" && part.audio_url) {
|
|
58
|
+
return { type: "audio", data: await llamaAudio(await bytesAt(part.audio_url.url, signal)) };
|
|
59
|
+
}
|
|
60
|
+
return part;
|
|
61
|
+
}));
|
|
62
|
+
return { ...message, content };
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The model as an OpenAI client: each request loads it with the adapter's
|
|
67
|
+
* settings (at once when it's loaded with them) and goes to wllama's
|
|
68
|
+
* `createChatCompletion`.
|
|
69
|
+
*/
|
|
70
|
+
function clientFor(model, settings, onDownloadProgress) {
|
|
71
|
+
const create = async (body, options) => {
|
|
72
|
+
const signal = options?.signal ?? undefined;
|
|
73
|
+
const wllama = await loadModel(model, settings, { ...(onDownloadProgress && { onDownloadProgress }), ...(signal && { signal }) });
|
|
74
|
+
const messages = await withMediaBytes(body.messages, signal);
|
|
75
|
+
return wllama.createChatCompletion({ ...body, messages, ...(signal && { abortSignal: signal }) });
|
|
76
|
+
};
|
|
77
|
+
return { baseURL: "wllama:", chat: { completions: { create } } };
|
|
78
|
+
}
|
|
79
|
+
/** The settings a model loads with, from the adapter's config. */
|
|
80
|
+
const loadSettings = ({ contextLength, wasmUrl, compat, threads, gpu, debug }) => ({
|
|
81
|
+
...(contextLength !== undefined && { contextLength }),
|
|
82
|
+
...(wasmUrl && { wasmUrl }),
|
|
83
|
+
...(compat && { compat }),
|
|
84
|
+
...(threads && { threads }),
|
|
85
|
+
...(gpu !== undefined && { gpu }),
|
|
86
|
+
...(debug && { debug })
|
|
87
|
+
});
|
|
88
|
+
export class WllamaTextAdapter extends OpenAIBaseChatCompletionsTextAdapter {
|
|
89
|
+
name = "wllama";
|
|
90
|
+
api = "wllama";
|
|
91
|
+
/** Text, and images or audio for a listed model whose projector reads them. */
|
|
92
|
+
inputModalities;
|
|
93
|
+
settings;
|
|
94
|
+
onDownloadProgress;
|
|
95
|
+
constructor(config, model) {
|
|
96
|
+
const settings = loadSettings(config);
|
|
97
|
+
super(model, "wllama", clientFor(model, settings, config.onDownloadProgress), { strictFallbackWarning: false });
|
|
98
|
+
this.settings = settings;
|
|
99
|
+
this.onDownloadProgress = config.onDownloadProgress;
|
|
100
|
+
this.inputModalities = listed(model) ? modelMeta(model).supports.input : ["text"];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Where the model stands in this browser, with the Prompt API's values:
|
|
104
|
+
* "available" once it's in wllama's storage (or loaded), "downloading"
|
|
105
|
+
* while this page downloads it, else "downloadable"; "unavailable" where
|
|
106
|
+
* wllama can't run.
|
|
107
|
+
*/
|
|
108
|
+
availability() {
|
|
109
|
+
return availability(this.model);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Downloads the model if it isn't stored and loads it with this adapter's
|
|
113
|
+
* settings, with the download's progress, 0 to 1, then 1 once it's ready.
|
|
114
|
+
* Resolves to this adapter; rejects if it can't load (fine to leave
|
|
115
|
+
* unawaited). `chat()` doesn't need it: its first call loads the model.
|
|
116
|
+
*/
|
|
117
|
+
create(options = {}) {
|
|
118
|
+
const own = this.onDownloadProgress;
|
|
119
|
+
const theirs = options.onDownloadProgress;
|
|
120
|
+
const ready = loadModel(this.model, this.settings, {
|
|
121
|
+
onDownloadProgress: (progress) => {
|
|
122
|
+
own?.(progress);
|
|
123
|
+
theirs?.(progress);
|
|
124
|
+
},
|
|
125
|
+
...(options.signal && { signal: options.signal })
|
|
126
|
+
}).then(() => this);
|
|
127
|
+
ready.catch(() => undefined);
|
|
128
|
+
return ready;
|
|
129
|
+
}
|
|
130
|
+
/** Unloads the model, freeing its memory. It stays in wllama's storage. */
|
|
131
|
+
async destroy() {
|
|
132
|
+
await unloadModel(this.model);
|
|
133
|
+
}
|
|
134
|
+
/** The request, with `modelOptions` as given over the model card's sampling, and the chat template's thinking switch. */
|
|
135
|
+
mapOptionsToRequest(options) {
|
|
136
|
+
const modelOptions = options.modelOptions;
|
|
137
|
+
const settings = {
|
|
138
|
+
...cardSampling(options.reasoning, this.model),
|
|
139
|
+
...modelOptions,
|
|
140
|
+
...(options.promptCache?.retention === "none" && { cache_prompt: false })
|
|
141
|
+
};
|
|
142
|
+
const request = super.mapOptionsToRequest({ ...options, modelOptions: settings, reasoning: undefined });
|
|
143
|
+
return Object.assign(request, thinkingKwargs(options.reasoning, this.model, modelOptions?.chat_template_kwargs));
|
|
144
|
+
}
|
|
145
|
+
/** llama.cpp streams thinking as `reasoning_content`. */
|
|
146
|
+
extractReasoning(chunk) {
|
|
147
|
+
const text = chunk.choices?.[0]?.delta?.reasoning_content;
|
|
148
|
+
return text ? { text } : undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* An audio part by its URL, as an image goes: Chat Completions takes
|
|
152
|
+
* audio only as base64 in two formats, and the client reads the bytes of
|
|
153
|
+
* either kind from a URL (a data URL for base64).
|
|
154
|
+
*/
|
|
155
|
+
convertContentPart(part) {
|
|
156
|
+
if (part.type !== "audio")
|
|
157
|
+
return super.convertContentPart(part);
|
|
158
|
+
const { source } = part;
|
|
159
|
+
if (source.type === "file") {
|
|
160
|
+
throw new Error("wllama takes audio as data or URLs, not as uploaded files.");
|
|
161
|
+
}
|
|
162
|
+
const url = source.type === "data" && !source.value.startsWith("data:")
|
|
163
|
+
? `data:${source.mimeType || "application/octet-stream"};base64,${source.value}`
|
|
164
|
+
: source.value;
|
|
165
|
+
return { type: "audio_url", audio_url: { url } };
|
|
166
|
+
}
|
|
167
|
+
/** Schemas as given: llama.cpp's grammar takes any JSON Schema, without OpenAI's strict-mode rewrite. */
|
|
168
|
+
makeStructuredOutputCompatible(schema) {
|
|
169
|
+
return schema;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A TanStack AI text adapter for a GGUF model that wllama runs in the
|
|
174
|
+
* browser.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* chat({ adapter: wllamaText("unsloth/Qwen3.5-2B-GGUF"), messages, threadId })
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function wllamaText(model, config = {}) {
|
|
182
|
+
return new WllamaTextAdapter(config, model);
|
|
183
|
+
}
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio as llama.cpp reads it. Its multimodal library decodes WAV, MP3 and
|
|
3
|
+
* FLAC files itself, and resamples them to the model's rate. Anything else
|
|
4
|
+
* a browser records or plays (Opus in WebM or Ogg, AAC in MP4) is decoded
|
|
5
|
+
* here, with Web Audio, and handed over as WAV.
|
|
6
|
+
*/
|
|
7
|
+
/** Whether `data` is a WAV, FLAC or MP3 file, by how it starts: the files llama.cpp decodes. */
|
|
8
|
+
export declare function decodesInLlama(data: ArrayBuffer): boolean;
|
|
9
|
+
/** Mono samples, each from -1 to 1, as a 16-bit PCM WAV file. */
|
|
10
|
+
export declare function wavFile(samples: Float32Array, sampleRate: number): ArrayBuffer;
|
|
11
|
+
/**
|
|
12
|
+
* An audio file's bytes as llama.cpp can decode them: as they are for WAV,
|
|
13
|
+
* MP3 and FLAC, else decoded by the browser and written as mono WAV.
|
|
14
|
+
*/
|
|
15
|
+
export declare function llamaAudio(data: ArrayBuffer): Promise<ArrayBuffer>;
|
|
16
|
+
//# sourceMappingURL=audio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAUzD;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAyB9E;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBxE"}
|
package/dist/audio.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio as llama.cpp reads it. Its multimodal library decodes WAV, MP3 and
|
|
3
|
+
* FLAC files itself, and resamples them to the model's rate. Anything else
|
|
4
|
+
* a browser records or plays (Opus in WebM or Ogg, AAC in MP4) is decoded
|
|
5
|
+
* here, with Web Audio, and handed over as WAV.
|
|
6
|
+
*/
|
|
7
|
+
/** The rate other formats are decoded at: the audio encoders' own, so llama.cpp resamples nothing. */
|
|
8
|
+
const SAMPLE_RATE = 16000;
|
|
9
|
+
/** Whether `bytes` holds `text` (ASCII) at `at`. */
|
|
10
|
+
const holds = (bytes, at, text) => [...text].every((character, i) => bytes[at + i] === character.charCodeAt(0));
|
|
11
|
+
/** Whether `data` is a WAV, FLAC or MP3 file, by how it starts: the files llama.cpp decodes. */
|
|
12
|
+
export function decodesInLlama(data) {
|
|
13
|
+
const bytes = new Uint8Array(data, 0, Math.min(data.byteLength, 12));
|
|
14
|
+
if (bytes.length < 4)
|
|
15
|
+
return false;
|
|
16
|
+
if (holds(bytes, 0, "RIFF") && holds(bytes, 8, "WAVE"))
|
|
17
|
+
return true;
|
|
18
|
+
if (holds(bytes, 0, "fLaC"))
|
|
19
|
+
return true;
|
|
20
|
+
// An MP3 that opens with an ID3 tag, or with a frame: 11 sync bits, then
|
|
21
|
+
// Layer III (AAC's ADTS frames open the same way, as layer 0).
|
|
22
|
+
if (holds(bytes, 0, "ID3"))
|
|
23
|
+
return true;
|
|
24
|
+
const [first = 0, second = 0] = bytes;
|
|
25
|
+
return first === 0xff && (second & 0xe0) === 0xe0 && (second & 0x06) === 0x02;
|
|
26
|
+
}
|
|
27
|
+
/** Mono samples, each from -1 to 1, as a 16-bit PCM WAV file. */
|
|
28
|
+
export function wavFile(samples, sampleRate) {
|
|
29
|
+
const size = samples.length * 2;
|
|
30
|
+
const file = new DataView(new ArrayBuffer(44 + size));
|
|
31
|
+
const text = (at, value) => {
|
|
32
|
+
for (let i = 0; i < value.length; i++)
|
|
33
|
+
file.setUint8(at + i, value.charCodeAt(i));
|
|
34
|
+
};
|
|
35
|
+
text(0, "RIFF");
|
|
36
|
+
file.setUint32(4, 36 + size, true);
|
|
37
|
+
text(8, "WAVE");
|
|
38
|
+
text(12, "fmt ");
|
|
39
|
+
file.setUint32(16, 16, true);
|
|
40
|
+
// PCM, one channel, 2 bytes a sample.
|
|
41
|
+
file.setUint16(20, 1, true);
|
|
42
|
+
file.setUint16(22, 1, true);
|
|
43
|
+
file.setUint32(24, sampleRate, true);
|
|
44
|
+
file.setUint32(28, sampleRate * 2, true);
|
|
45
|
+
file.setUint16(32, 2, true);
|
|
46
|
+
file.setUint16(34, 16, true);
|
|
47
|
+
text(36, "data");
|
|
48
|
+
file.setUint32(40, size, true);
|
|
49
|
+
samples.forEach((value, i) => {
|
|
50
|
+
const sample = Math.max(-1, Math.min(1, value));
|
|
51
|
+
file.setInt16(44 + i * 2, Math.round(sample < 0 ? sample * 0x8000 : sample * 0x7fff), true);
|
|
52
|
+
});
|
|
53
|
+
return file.buffer;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* An audio file's bytes as llama.cpp can decode them: as they are for WAV,
|
|
57
|
+
* MP3 and FLAC, else decoded by the browser and written as mono WAV.
|
|
58
|
+
*/
|
|
59
|
+
export async function llamaAudio(data) {
|
|
60
|
+
if (decodesInLlama(data))
|
|
61
|
+
return data;
|
|
62
|
+
if (typeof OfflineAudioContext === "undefined") {
|
|
63
|
+
throw new Error("This audio isn't WAV, MP3 or FLAC, and there's no Web Audio here to decode it.");
|
|
64
|
+
}
|
|
65
|
+
let decoded;
|
|
66
|
+
try {
|
|
67
|
+
// A copy: decoding detaches the buffer it's given.
|
|
68
|
+
decoded = await new OfflineAudioContext(1, 1, SAMPLE_RATE).decodeAudioData(data.slice(0));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error("The browser couldn't decode this audio. WAV, MP3 and FLAC always work.");
|
|
72
|
+
}
|
|
73
|
+
const mono = new Float32Array(decoded.length);
|
|
74
|
+
for (let channel = 0; channel < decoded.numberOfChannels; channel++) {
|
|
75
|
+
decoded.getChannelData(channel).forEach((sample, i) => {
|
|
76
|
+
mono[i] = (mono[i] ?? 0) + sample / decoded.numberOfChannels;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return wavFile(mono, decoded.sampleRate);
|
|
80
|
+
}
|