web-vision-detect is a kit - a reusable building block added into an existing web app. It runs YOLOX (Apache-2.0) on ONNX Runtime Web so an app can read the camera (or a still image) and do real-time object detection entirely in the browser - boxes, labels, and confidence for the 80 COCO classes, or for a custom-trained model the user brings.

On-device: no server, no upload, the camera stream never leaves the device. Inference runs on WebGPU where the browser has it, with automatic WASM (SIMD) fallback. Web only - it needs getUserMedia (for camera use), WASM, and a canvas, so it runs only on HTTPS or localhost.

This kit is the high-accuracy sibling of web-vision-mediapipe: use that one for gesture and pose, this one when detection is the product - counting, labeling, inventory, custom classes.

Two ways in

Start a fresh detection app - add the object-spotter starter, a fullscreen camera app with the kit pre-installed that boxes, labels, and counts objects live, detects picked photos, and switches between three speed/accuracy presets:

add name=object-spotter title="..."

Add detection to an existing web app - install the kit into it:

add name=web-vision-detect

This copies the kit to src/packages/web-vision-detect/ and wires the import map in src/index.html (the kit specifier plus onnxruntime-web). There is no deploy phase - it is pure client-side, so a plain static app needs nothing else.

Using the kit

The whole job is two elements - a <video> for the camera and a <canvas> overlaying it - plus one call:

import { mountDetect } from '@gipity/web-vision-detect';

const vision = await mountDetect({
  video:  document.querySelector('video'),
  canvas: document.querySelector('canvas'),
  model:  'nano',                             // 'nano' | 'tiny' | 's' | custom spec
  camera: { facingMode: 'environment' },      // rear camera is the default here
  onFps:  (fps) => { hud.textContent = `${fps} FPS`; },
  onResult: ({ detections }) => { /* app logic - shape below */ },
});

await vision.switchModel('s');     // trade frame rate for accuracy
const r = await vision.detect(img); // one-off detection on an <img> or canvas
vision.stop();                      // release camera + free model memory

Each detection is { label, classId, score, box: { x, y, width, height } } in source-frame pixels - drawing on a canvas sized to the frame lines up 1:1 (mountDetect already draws boxes; onResult is for app logic like counting).

Detections arrive sorted by descending score, and suppression is class-aware - a box only suppresses an overlapping box of the same classId. So one real-world object can surface as several overlapping detections with different labels: an ambiguous animal yields both a cat box and a dog box. Don't assume one detection per object. When a class must not fire for a look-alike (dog but never cat), compare the overlapping boxes' scores and require a margin, rather than acting on the first matching label you find.

For a custom loop, compose the low-level exports instead: createDetector, startCamera, createLoop, drawDetections, plus pure-math decodeYolox / decodeYolo / nms. See src/packages/web-vision-detect/examples/ and its README.md.

Models

model Download COCO mAP Use when
nano (default) 3.7 MB 25.8 Instant start, phones, casual demos
tiny 20 MB 32.8 Noticeably better accuracy, still fast
s 36 MB 40.5 Accuracy is the point; fine on WebGPU

Presets are official YOLOX exports hosted on the Gipity CDN, fetched on first use and browser-cached. Custom models: pass model: { url, format, inputSize, labels } - format: 'yolox' for YOLOX exports, format: 'yolo' for Ultralytics YOLOv8/v11 model.export(format='onnx'). This is the deploy path for "I trained a detector on Roboflow/Ultralytics and want it in an app".

Notes and common mistakes