Docs: run AI models in the browser with runonweb
runonweb is a set of ES modules. Each one picks WebGPU or WASM, downloads the right weights, and decodes inputs for you.
Install runonweb
pnpm add runonweb
npm install runonweb
bun add runonweb
yarn add runonwebRequires a bundler that understands package exports (Vite, Next, Astro, Bun…). Weights are fetched from Hugging Face at runtime and cached by the browser.
The pattern: load, run, dispose
Every module follows the same shape:
const m = new Module({ onProgress })
await m.load() // downloads + caches weights, safe to call twice
await m.<task>(input) // the actual work
m.dispose() // free memoryEach module also exports a one-shot function (transcribe, removeBackground,caption, read…) that loads, runs and disposes in one call. Use the class when you will run more than once.
Core: WebGPU detection
import { isWebGPUAvailable, resolveDevice } from 'runonweb/core'
const ok = await isWebGPUAvailable()
const device = await resolveDevice() // 'webgpu' | 'wasm'Scan : OCR
Live demo →Extract printed and scene text in the browser. Three sizes (tiny, small, medium) so you can trade a few megabytes for accuracy. Nothing is uploaded.
import { OCR } from 'runonweb/ocr'
const ocr = new OCR({
size: 'small', // tiny | small | medium
onProgress: (info) => console.log(info.status, info.progress),
})
await ocr.load()
const { text, lines } = await ocr.read(imageFile)
console.log(text)
// lines: [{ text, score, box: { xmin, ymin, xmax, ymax } }]
ocr.dispose()- Base ·
- PP-OCRv6 by PaddlePaddle
- License ·
- Apache-2.0
- Download ·
- ~31 MB (small) · tiny ~6 MB · medium ~139 MB
- Runs on ·
- fp32 on WebGPU · fp32 on WASM
- Default size is
small(~31 MB): the best size/quality trade-off. Passtiny(~6 MB) ormedium(~139 MB). smallandmediumcover 50 languages (Chinese, English, Japanese, Latin).tinyis Chinese + English.- Each line comes back with a box and a confidence score.
Remover : Background removal
Live demo →Remove image backgrounds client-side. Drop a photo and get a PNG with an alpha channel. Private by default, and offline-capable once the model is cached.
import { RemoveBackground } from 'runonweb/remove-bg'
const remover = new RemoveBackground({
onProgress: (info) => console.log(info.status, info.progress),
})
await remover.load()
// Blob, File, URL, HTMLImageElement, ImageData, or canvas
const png = await remover.remove(imageFile)
// png is a PNG Blob with alpha channel
document.querySelector('img').src = URL.createObjectURL(png)
remover.dispose()- Base ·
- BEN2 by Prama LLC
- License ·
- MIT
- Download ·
- ~219 MB
- Runs on ·
- fp16 on WASM
- BEN2 (2025) is a general background eraser: hair, fur, products and hard edges, not just portraits.
- ~219 MB of fp16 weights, cached after the first download.
- Output is a PNG Blob with alpha, same size as the input.
- Runs on WASM. WebGPU dies in BEN2’s LayerNorm shader (fp16 activation, fp32 scale and bias) on current onnxruntime-web. The fix is upstream (onnxruntime#32629, 2026-09-22) and not published yet.
- Sample photos: Messi by Bryan Berlin (CC BY-SA 4.0), alfajores by V!NZ (CC BY-SA 3.0). Both resized.
Scribe : Speech-to-Text
Live demo →Transcribe audio to text entirely in the browser. Record from the mic or drop a file. Nothing is uploaded, nothing is billed per minute.
import { SpeechToText } from 'runonweb/stt'
const stt = new SpeechToText({
onProgress: (info) => console.log(info.status, info.progress),
})
await stt.load()
// Blob, File, URL string, or Float32Array (16 kHz mono)
const { text, chunks } = await stt.transcribe(audioBlob, {
timestamps: 'word',
onPartial: (partial) => console.log(partial),
})
console.log(text, chunks[0])
stt.dispose()- Base ·
- Whisper tiny.en by OpenAI
- License ·
- Apache-2.0
- Download ·
- ~154 MB / ~44 MB
- Runs on ·
- fp32 on WebGPU · q8 on WASM
- English-only by default. Pass
model(e.g.onnx-community/whisper-tiny) andlanguage(es,ja…) for a multilingual Whisper variant. Withoutlanguage, tiny often guesses English and the transcript comes out translated. The live demo always usestask: "transcribe"and lets you pick the language. - WebGPU is several times faster; WASM works everywhere else.
- Audio is decoded and resampled to 16 kHz mono in the browser.
- Pass
timestamps: "segment"for each phrase with the model's start and end, in seconds."word"(ortrue) splits those phrases into words and keeps every word inside its phrase. This ONNX Whisper has no cross-attentions, so it cannot time a word on its own. Omittimestampsandchunksis empty. - Pass
onPartialto receive words as they are generated.
Alt : Image captioning
Live demo →Describe any image in one sentence, in English, Spanish, Portuguese, French, German, Arabic, Chinese, Japanese or Korean. Useful for alt text, search indexing and accessibility. Generated on-device, no image ever leaves the page.
import { ImageCaptioner } from 'runonweb/caption'
const captioner = new ImageCaptioner({ language: 'es', detail: 'short' })
await captioner.load()
const { text } = await captioner.caption(imageFile)
// "Un gato sentado sobre una mesa de madera."
// Per call overrides, streamed
await captioner.caption(imageFile, {
language: 'en',
detail: 'detailed',
onPartial: (partial) => console.log(partial),
})
captioner.dispose()- Base ·
- LFM2.5-VL-450M by Liquid AI
- License ·
- LFM Open License v1.0 (free < USD 10M revenue)
- Download ·
- ~548 MB / ~508 MB
- Runs on ·
- fp16+q4f16 on WebGPU · fp16+q8+q4 on WASM
- LFM2.5-VL-450M (Liquid AI, Nov 2025). A small vision-language model, prompted for captions, not a dedicated captioner.
- Pass
language(en,es,pt,fr,de,ar,zh,ja,ko) to caption in that language. Other languages are best-effort. - Pass
detail: "detailed"or"more"for longer captions. Default is short alt text.onPartialstreams the text as it is generated. - License: LFM Open License v1.0. Free for individuals and companies under USD 10M annual revenue. Above that, Liquid AI requires a commercial license. Not OSI-approved.
- WebGPU is the fast path (fp16 vision + q4f16 decoder, about a second per caption). GPUs without
shader-f16load fp32 vision + q4 decoder (~909 MB). WASM works but takes ~30 s per caption. - It is a generative model: captions can hallucinate details. Review before publishing.
Depth : Depth estimation
Live demo →Turn a single photo into a depth map. Great for parallax effects, portrait blur or 3D-ish previews without any server round-trip.
import { DepthEstimator } from 'runonweb/depth'
const estimator = new DepthEstimator()
await estimator.load()
const { depth, width, height } = await estimator.estimate(imageFile)
// depth is a grayscale PNG Blob
document.querySelector('img').src = URL.createObjectURL(depth)
estimator.dispose()- Base ·
- Depth Anything V2 Small by DepthAnything (HKU / TikTok)
- License ·
- Apache-2.0
- Download ·
- ~50 MB / ~27 MB
- Runs on ·
- fp16 on WebGPU · q8 on WASM
- Output is a grayscale PNG: brighter means closer.
- The Small variant is Apache-2.0; larger Depth Anything models are not.
- Sample photos, resized: Obelisco by Roberto Fiadone (CC BY-SA 4.0), Congreso by Jorge Royan (CC BY-SA 3.0), Caminito by Lars Curfs (CC BY-SA 3.0 NL), Palacio Barolo by Beatrice Murch (CC BY 2.0).
Spot : Object detection
Live demo →Find and label objects in an image with bounding boxes. People, cars, cats, cups, laptops: 80 COCO classes detected locally with RF-DETR Nano.
import { ObjectDetector } from 'runonweb/detect'
const detector = new ObjectDetector({
threshold: 0.5, // min confidence
})
await detector.load()
const objects = await detector.detect(imageFile)
// [{ label: 'cat', score: 0.98, box: { xmin, ymin, xmax, ymax } }]
detector.dispose()- Base ·
- RF-DETR Nano by Roboflow
- License ·
- Apache-2.0
- Download ·
- ~29 MB
- Runs on ·
- q8 on WASM
- RF-DETR Nano (2025). 80 COCO classes: people, cars, animals, furniture and more.
- WASM only for now. WebGPU is skipped: this ONNX export collapses confidence scores.
- Boxes are returned in pixel coordinates of the original image.
- Tune
thresholdto trade recall for precision.
Vector : Text embeddings
Live demo →Turn sentences into 384-dimensional vectors for semantic search, clustering or deduplication. Compare meaning, not keywords, in the browser, in milliseconds.
import { TextEmbedder, cosineSimilarity } from 'runonweb/embed'
const embedder = new TextEmbedder()
await embedder.load()
const { embeddings } = await embedder.embed([
'How do I reset my password?',
'I forgot my login credentials',
'What is the weather today?',
])
cosineSimilarity(embeddings[0], embeddings[1]) // ~0.7
cosineSimilarity(embeddings[0], embeddings[2]) // ~0.0
embedder.dispose()- Base ·
- all-MiniLM-L6-v2 by sentence-transformers
- License ·
- Apache-2.0
- Download ·
- ~46 MB / ~24 MB
- Runs on ·
- fp16 on WebGPU · q8 on WASM
- Vectors are L2-normalized by default, so cosine similarity is a dot product.
- Works best on English; pass a multilingual model via
modelif needed.
Classify : Text classification
Live demo →Route a ticket, rate a review or flag a comment: yes/no, multiple-choice and rating questions about one text, answered with calibrated probabilities in a single call. Runs Kev, an open Jev-style decision model, in the tab. No text is generated.
import { Classifier } from 'runonweb/classify'
const classifier = new Classifier() // size: 'large' for Kev-4B (~2.7 GB)
await classifier.load()
const { answers } = await classifier.classify({
state: 'Shoes arrived two weeks late and in the wrong size.',
questions: {
department: {
type: 'choice',
instructions: 'Which team should handle this?',
criteria: { returns: null, shipping: null, billing: null },
},
escalate: { type: 'noul', instructions: 'Does this need urgent human attention?' },
frustration: {
type: 'score',
instructions: 'How frustrated is the customer?',
criteria: ['Calm', 'Frustrated', 'Very angry'],
},
},
})
answers.department.choice // "returns"
answers.department.probabilities // { returns: 0.50, shipping: 0.44, billing: 0.06 }
answers.escalate.noul // 0.51 = p(yes)
answers.frustration.score // 1.21 on a 0–2 scale
classifier.dispose()- Base ·
- Kev-0.8B (Qwen3.5-0.8B-Base + LoRA) by Jared Palmer · base: Qwen
- License ·
- Apache-2.0
- Download ·
- ~750 MB · large ~2.7 GB
- Runs on ·
- int4 on WebGPU · same files on WASM
- Kev-0.8B by Jared Palmer: a LoRA and a pointer head on Qwen3.5-0.8B-Base, trained to answer typed questions. Each question scores its options in one forward pass; nothing is generated, so answers are probabilities, not text.
- Request and answer shapes are TypeSafe’s System One API (
noul·choice·score), the same JSON a Kev or Jev server accepts. Question ids are yours; the model never sees them. - Each question only sees the text and itself. The text is read once and its cache is reused for every question.
- Probabilities are calibrated with the temperature fitted for the checkpoint (~2.35). Pass
temperature: 1for raw logits. Option order can still change an answer. - Kev can’t subtract dates.
dateFacts: trueappends the day count between every pair of absolute dates in the text, as Kev’sKEV_DATE_FACTSdoes. - Two sizes.
small(default) is Kev-0.8B: 0.70 on sources it wasn’t trained on, weak on general knowledge, best in English.size: "large"is Kev-4B: 0.84 (Jev: 0.86), ~2.7 GB and about 4× slower. Test either on your own data before trusting a threshold. - runonweb merged the LoRA and exported the backbone to ONNX: int4 weights with fp32 activations (Kev-0.8B keeps its Gated DeltaNet layers in int8). Probabilities differ from the fp32 export by ~0.03 (small) and ~0.04 (large) on average. Recipe in
training/kev-onnx.
Lingo : Translation
Live demo →Translate text entirely in the browser. 21–55 MB per language pair, 58 languages to and from English, plus pairs like Spanish → Italian or Japanese → French through English. Private, offline after the first load, and free of per-character pricing.
import { Translator, PAIRS } from 'runonweb/translate'
// en → es: base-memory, ~37 MB
const translator = new Translator({ from: 'en', to: 'es' })
await translator.load()
const { text } = await translator.translate('Hello world')
// "Hola mundo"
// same instance, another pair (downloaded on demand)
await translator.translate('Bonjour', { from: 'fr', to: 'en' })
// no direct es-it model: pivots through English (es-en + en-it)
await translator.translate('¿Dónde está la estación?', { from: 'es', to: 'it' })
// keep the markup, translate the text nodes
await translator.translate('<b>Hello</b> world', { html: true })
translator.dispose()
PAIRS['en-ja'] // { architecture: 'base-memory', bytes: 43849787, comet: 0.90, … }- Base ·
- Firefox Translations (Marian NMT) by Mozilla
- License ·
- MPL-2.0
- Download ·
- ~22 MB (tiny) · ~37 MB (base-memory) · ~50–55 MB (ja, ko, zh)
- Runs on ·
- int8 on WASM (Bergamot)
- Marian NMT students distilled from a larger teacher, run through the Bergamot WASM runtime (int8 GEMM, single worker, no COOP/COEP headers needed).
base-memorywhen available (≈37 MB, on par with cloud translators within ~5% COMET),tinyfor the rest (≈22 MB).PAIRSlists every pair with size and COMET score.- Pairs without a direct model pivot through English:
es → frloadses-enanden-fr. - Pass
html: trueto translate markup while keeping the tags in place. - Codes are BCP 47:
zhis Simplified Chinese,zh-HantTraditional. OneTranslatorcan serve many pairs; new pairs download on demand. - Prefer one model for 100 languages? Pass
model: "Xenova/m2m100_418M"(MIT, ~640 MB, Transformers.js, slower).
Polish : Transcript cleanup
Live demo →Clean a speech-to-text transcript in the tab: drop fillers, resolve self-corrections, write numbers and emails, and apply punctuation. Pair it with Scribe, or paste any rough transcript.
import { TranscriptCleaner } from 'runonweb/clean'
const cleaner = new TranscriptCleaner({
styling: 'semi-formal',
onProgress: (info) => console.log(info.status, info.progress),
})
await cleaner.load()
const { text } = await cleaner.clean(
'so um i need to like send the the report by uh friday no wait make that thursday',
{ onPartial: (partial) => console.log(partial) },
)
// "I need to send the report by Thursday."
cleaner.dispose()- Base ·
- S1-mini by Superwhisper
- License ·
- Apache-2.0 + naming term
- Download ·
- ~365 MB / ~413 MB
- Runs on ·
- q4f16 on WebGPU · q4 on WASM
- S1-mini by Superwhisper. Its license is Apache-2.0 plus one term: any product that uses it must keep calling it "S1-mini" by "Superwhisper". Fine-tuned from Qwen3-0.6B to do one job: normalize English ASR output. It is not a chat model.
- Pass
styling(casual·semi-casual·semi-formal·formal),structure(prose·lists) andcontext(general·email). Defaults are semi-formal prose. - English only. Keep a single pass under ~1,000 tokens; chunk longer transcripts at sentence boundaries.
- Filler-only input (
um,uh) returns an empty string. That is a valid result, not a failure. - WebGPU prefers q4f16 (~365 MB) and falls back to q4 (~413 MB) if the session fails to start. WASM uses q4.
Emojify : Text-to-Emoji
Live demo →Translate any English sentence into a short emoji sequence with a 2.4M-parameter model trained by runonweb. Loads in under a second, runs in a few milliseconds and never leaves the tab.
import { Emojifier } from 'runonweb/emoji'
// 3.9 MB from the Hugging Face Hub (midudev/text2emoji-tiny)
const emojifier = new Emojifier()
await emojifier.load()
const { text, emojis } = await emojifier.emojify('I love pizza and my dog')
// text "🍕❤️🐶"
// emojis ["🍕", "❤️", "🐶"]
emojifier.dispose()- Base ·
- text2emoji-tiny (T5, 2.4M params, from scratch) by runonweb · data: Text2Emoji (KomeijiForce)
- License ·
- MIT
- Download ·
- ~4 MB
- Runs on ·
- q8 on WASM
- Trained from scratch: a 3-layer T5 (d_model 128, 8k shared vocab) on ~490k text/emoji pairs from the Text2Emoji dataset. Recipe in
training/text2emoji. - English input. Output is a short sequence of distinct emojis;
maxEmojiscaps the length (default 12). - Weights download from the Hugging Face Hub (
midudev/text2emoji-tiny). To self-host, copy the folder and passmodelPathplusmodel(the folder name).
Voice : Text-to-Speech
Live demo →Synthesize speech from text on-device. Three sizes: Kokoro 82M for English, Spanish and French, Supertonic 2 for English, Korean, Spanish, Portuguese and French at 44.1 kHz, or KittenTTS nano at ~28 MB. Stream PCM as it is generated, or download a WAV.
import { TextToSpeech } from 'runonweb/tts'
const tts = new TextToSpeech({ size: 'small', voice: 'af_heart' })
await tts.load()
for await (const chunk of tts.speakStream('Hello from the browser')) {
// chunk.audio is 24 kHz PCM. Play as it arrives
}
const wav = await tts.speakToBlob('Hola mundo', { voice: 'ef_dora' })
new Audio(URL.createObjectURL(wav)).play()
// Supertonic 2: one model, five languages, 44.1 kHz
const multi = new TextToSpeech({ size: 'multi', voice: 'st_f1' })
const pt = await multi.speakToBlob('Olá do navegador', { language: 'pt' })
tts.dispose()- Base ·
- Kokoro 82M by hexgrad
- License ·
- Apache-2.0 · OpenRAIL-M (multi)
- Download ·
- ~326 MB / ~93 MB (small) · multi ~263 MB · tiny ~28 MB
- Runs on ·
- fp32 on WebGPU · q8 on WASM (multi is fp32, tiny is WASM)
- Default
smallis Kokoro 82M (StyleTTS 2): American, British, Spanish and French. Runs on Transformers.js directly, nokokoro-js. fp32 on WebGPU (fp16 gives NaN on the v4 runtime), q8 on WASM. - Pass
size: "multi"for Supertonic 2 (Supertone, 2026): one ~263 MB model for English, Korean, Spanish, Portuguese and French at 44.1 kHz, 10 preset voices (st_f1…st_m5). Passlanguageper call. - Supertonic 2 is OpenRAIL-M: open weights with use restrictions (no impersonation, no deception, no illegal use). The rest of the TTS sizes are Apache-2.0.
- Pass
size: "tiny"for KittenTTS nano (~28 MB, 8 English voices, WASM). - Audio streams sentence by sentence via
speakStream. WebGPU is several times faster onsmallandmulti; WASM works everywhere. - Pass
voiceto pick a speaker (af_heart/bella/st_f1). Kokoro Spanish and French download a local eSpeak-NG WASM (~18 MB) on first use. Nothing is uploaded.
Imagine : Image generation
Live demo →Generate images from a text prompt entirely in the tab. Bonsai Image 4B runs on WebGPU. No API key, no upload, nothing leaves the device after the first download.
import { ImageGenerator } from 'runonweb/image'
const gen = new ImageGenerator({ size: 'binary' })
await gen.load()
const { image, seed } = await gen.generate(
'A bonsai tree in a quiet ceramic studio, soft morning light',
)
document.querySelector('img').src = URL.createObjectURL(image)
gen.dispose()- Base ·
- Bonsai Image 4B (FLUX.2 Klein) by Prism ML
- License ·
- Apache-2.0
- Download ·
- ~3.4 GB (binary) · ternary ~3.9 GB
- Runs on ·
- 1-bit / 1.58-bit on WebGPU
- Bonsai Image 4B (Prism ML, 2026) is a 1-bit / 1.58-bit FLUX.2 Klein deployment. WebGPU only. There is no WASM path.
- Default
binaryis the smaller payload (~3.4 GB). Passsize: "ternary"(~3.9 GB) for the quality-oriented weights. - Tuned for 4 FlowMatch-Euler steps at guidance 1.0. More steps rarely help and can add artifacts. Negative prompts are not used.
- Default output is 512×512. Native training resolution is 1024×1024; sides must be multiples of 16 (32 recommended).
- First download is several gigabytes and is cached in IndexedDB. Chromium + a recent GPU is required.