kjarni-wasm

中文文档
0.4.3发布于 2026/11/04MIT OR Apache-2.0TypeScriptES Module零依赖

Run transformer models inside your web page: embeddings, classification, reranking, semantic search and chat in a Web Worker. Same output as PyTorch, no server.

原始使用文档
文档目录12 节
  1. Copy this and it runs
  2. Quick tour
  3. Why a worker
  4. Loading models
  5. Classification
  6. Reranking
  7. Chat
  8. Cleaning up
  9. Requirements
  10. Models
  11. The same engine elsewhere
  12. Making your own .kjq

kjarni: local AI inference in a single native library

kjarni-wasm

Run transformer models inside your web page. Same output as PyTorch, no server.

Kjarni is a transformer inference library, and this is its WebAssembly build: embeddings, classification, reranking, semantic search and chat in the browser, running in a Web Worker so the page never freezes. The same engine ships for Python, C#, Go, C++ and Rust.

npm i kjarni-wasm

Or without a bundler, straight from a CDN:

<script type="module">
  import { Kjarni } from "https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js";
</script>

Copy this and it runs

One file, nothing installed. Save it as quickstart.html, serve the directory, open it: a real model downloads and ranks three sentences by meaning.

curl -sO https://raw.githubusercontent.com/olafurjohannsson/kjarni/main/crates/kjarni-wasm/examples/quickstart.html
python3 -m http.server 8000
# then open http://localhost:8000/quickstart.html

A server is needed because ES modules do not load over file://. Any static server will do.

<pre id="out">starting…</pre>
<script type="module">
  import init, { WasmModel } from "https://cdn.jsdelivr.net/npm/[email protected]/pkg/kjarni_wasm.js";
  await init();

  const url = "https://huggingface.co/olafuraron/all-MiniLM-L6-v2-q8/resolve/main/all-MiniLM-L6-v2-q8.kjq";
  const model = WasmModel.from_quantized(new Uint8Array(await (await fetch(url)).arrayBuffer()));

  const texts = [
    "How do I get my money back?",
    "What is your refund policy?",
    "The weather in Reykjavik is unpredictable.",
  ];

  // Normalised, so a dot product is the cosine similarity.
  const flat = model.encode(texts, true);
  const dim = flat.length / texts.length;
  const vec = (i) => flat.subarray(i * dim, (i + 1) * dim);
  const cosine = (a, b) => a.reduce((sum, x, i) => sum + x * b[i], 0);

  out.textContent = `related:   ${cosine(vec(0), vec(1)).toFixed(4)}\n`
                  + `unrelated: ${cosine(vec(0), vec(2)).toFixed(4)}`;
</script>
related:   0.5268
unrelated: -0.0470

This imports the wasm module directly rather than the Kjarni wrapper, so it runs on the main thread with no Worker: fine for a page that encodes a few sentences, not for chat. Use Kjarni for anything larger, and see the Chat section.

Scores differ slightly from the native library (0.5510 / -0.0630) because the browser model is int8-quantised. The ranking is the same, which is what a search uses.

Quick tour

import { Kjarni } from "kjarni-wasm";

const kjarni = await Kjarni.load({
  encoder: "https://huggingface.co/olafuraron/all-MiniLM-L6-v2-q8/resolve/main/all-MiniLM-L6-v2-q8.kjq",
});

const [doctor, physician] = await kjarni.encode(["doctor", "physician"]);
console.log(Kjarni.cosine(doctor, physician));   // 0.8598

// or in one call
console.log(await kjarni.similarity("doctor", "physician"));

That URL is a real, public 23 MB file. It downloads once, the browser caches it, and nothing else leaves the machine. See Models for the rest, and serve them from your own origin in production rather than hotlinking.

Every method returns a promise and the work happens on a worker thread. A model loads or a reply generates while the page keeps painting.

Why a worker

Inference is compute-bound and synchronous. On the main thread, encoding a batch freezes the tab for as long as it takes, and generating a chat reply freezes it for seconds. That is the single most common way a browser AI feature ends up feeling broken.

This package puts all of it on a worker and gives you promises, so you never have to think about it.

Loading models

Models are .kjq files: config, tokenizer and int8 weights in one download.

const kjarni = await Kjarni.load({
  encoder:    model("all-MiniLM-L6-v2-q8"),
  classifier: model("distilbert-sentiment-q8"),
  reranker:   model("ms-marco-MiniLM-L-6-v2-q8"),
  onProgress: (capability, loaded, total) => {
    console.log(`${capability}: ${Math.round((loaded / total) * 100)}%`);
  },
});

Load only what you use. Each model is a real download, from 23 MB for MiniLM to several hundred for a chat model, so onProgress is worth wiring to a progress bar.

Bytes are transferred to the worker rather than copied, which matters: copying a 500 MB model is a visible pause and doubles peak memory.

Classification

const [result] = await kjarni.classify("This is wonderful");
console.log(result.label, result.score);   // POSITIVE 0.9999

const batch = await kjarni.classify(["Great service", "Worst purchase ever"]);
console.log(await kjarni.labels());        // ["NEGATIVE", "POSITIVE"]

Reranking

Scores query and document together, which is markedly more accurate than comparing embeddings. Indices point back into your input, so you keep your own ids.

const ranked = await kjarni.rerank("how do I cancel?", documents, 5);
for (const { index, score } of ranked) console.log(score, documents[index]);

Chat

A 0.5B language model runs in the tab, and tokens arrive as they are decoded.

const kjarni = await Kjarni.load({
  chat: model("qwen05b-q8"),
  chatModelId: "qwen2.5-0.5b-instruct",
});

let out = "";
for await (const piece of kjarni.chat("Explain RAG in one sentence.")) out += piece;

This works because the weights stay block-quantised in memory. wasm32 caps a single allocation at isize::MAX, which is 2 GB, and Qwen2.5 0.5B expanded to f32 needs 1.98 GB, so a loader that dequantises up front traps on load. The .kjq container carries block-quantised weights the engine reads in place, which is 500 MB rather than 1.98 GB, and decodes faster than f32 rather than slower.

Expect a few tokens per second on a laptop CPU. Run it in a Web Worker: generation occupies the thread until it returns, so on the main thread the tab stops painting for the whole reply.

chat() completes a prompt; it does not keep turns. Multi-turn conversation, where the model's own chat template is applied and earlier turns are remembered, exists on WasmChat (send_stream, clear_history) and is not yet surfaced through this wrapper.

Cleaning up

await kjarni.close();

Wasm handles are not reachable by the JavaScript garbage collector, so dropping the reference leaks the weights. close() frees the models and stops the worker.

Requirements

A browser with WebAssembly and module workers: Chrome and Edge 80+, Firefox 114+, Safari 15+. No WebGPU required, inference runs on the CPU.

Models

.kjq is a single file holding config, tokenizer and int8 weights. These are public and hosted on Hugging Face, one repository per model, with the repository and file names matching:

const model = (name: string) =>
  `https://huggingface.co/olafuraron/${name}/resolve/main/${name}.kjq`;
Pass asNameSizeWhat it does
encoderall-MiniLM-L6-v2-q823 MBembeddings, similarity, search
rerankerms-marco-MiniLM-L-6-v2-q823 MBcross-encoder reranking
classifierdistilbert-sentiment-q868 MBpositive / negative sentiment
chatqwen05b-q8508 MBa local language model, block-quantised so it fits

Hugging Face serves these with permissive CORS, so a browser can fetch them from any origin. For production, copy what you need and serve it from your own origin: you control caching, and your page does not depend on someone else's uptime.

Load only what you use, and wire onProgress to something visible: these are real downloads, and a silent minute reads as a broken page.

The same engine elsewhere

Kjarni is one Rust engine behind several packages. The browser is one target; if you need the server half of the same product:

Making your own .kjq

See the format guide. Any BERT-family encoder, cross-encoder, classifier or small decoder from Hugging Face converts with one script.

版本与发布记录

kjarni-wasm 共发布 22 个版本,首次发布于 2026/08/31;过去 12 个月发布了 22 个版本。

版本发布日期距今
0.4.3latest2026/11/041 天前
0.4.22026/11/031 天前
0.4.12026/11/032 天前
0.4.02026/11/013 天前
0.3.02026/11/014 天前
0.2.92026/10/314 天前

关于 kjarni-wasm 的常见问题

kjarni-wasm 是什么?

Run transformer models inside your web page: embeddings, classification, reranking, semantic search and chat in a Web Worker. Same output as PyTorch, no server.

如何安装 kjarni-wasm?

在项目目录运行 npm install kjarni-wasm,或使用 pnpm add / yarn add / bun add。

kjarni-wasm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

kjarni-wasm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

kjarni-wasm 可以商用吗?

kjarni-wasm 使用 MIT OR Apache-2.0 许可。可在 MIT、Apache-2.0 中任选其一遵守。选择 MIT 时,宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

kjarni-wasm 还在维护吗?

最新版本 0.4.3 发布于 2026/11/04(1 天前);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/08/31。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。