# Local AI Image Detector A Chrome Manifest V3 extension that detects AI-generated images on any webpage, entirely inside the browser. No cloud inference, no external APIs, no local server — every tensor operation runs as a **hand-written WebGPU compute kernel** written directly against the WebGPU API (no ONNX Runtime, no transformers.js, no WASM inference runtime). ## How it works - **Model**: [`buildborderless/CommunityForensics-DeepfakeDet-ViT`](https://huggingface.co/buildborderless/CommunityForensics-DeepfakeDet-ViT) — a ViT-Small/16 vision transformer trained on 2.7M images across 4,803 generators ([Community Forensics, CVPR 2025](https://arxiv.org/abs/2411.04125)), fine-tuned as a binary real/fake classifier with a single sigmoid logit. - **One-time download**: on first use, the extension downloads the model's `model.safetensors` weights (~87MB) directly from Hugging Face and caches them in IndexedDB. Every run after that reads only from the local cache — the extension never re-downloads or fetches any other model/weight file. - **Inference**: the entire ViT forward pass (patch embedding, 12 transformer encoder blocks, classifier head) is implemented from scratch as WGSL compute shaders in [`extension/src/lib/shaders.js`](extension/src/lib/shaders.js) and orchestrated in [`extension/src/lib/vit-engine.js`](extension/src/lib/vit-engine.js) — a register-tiled batched GEMM, layernorm, exact GELU, multi-head attention and softmax, all running as native WebGPU pipelines. - **Multi-scale scoring**: the detector reads pixel-level generator fingerprints, so it is very sensitive to what resampling an image has been through. Each image is scored at several views (the processor-faithful resize-and-crop, plus 384x384 centre crops at 1/1, 1/2 and 1/4 scale) and the strongest response wins — see [`preprocess.js`](extension/src/lib/preprocess.js). - **Calibration**: the model's raw sigmoid is not centred on 0.5; its decision boundary sits near 1%. A monotonic logit-space shift maps that operating point onto the displayed 65% threshold, so the badge number means what it says — see [`calibration.js`](extension/src/lib/calibration.js). - **On-page detection**: a content script finds `` elements as they scroll into view and sends their URL to the background worker, which forwards it to the offscreen document. The offscreen document — an extension page with the same `host_permissions`-granted, CORS-free fetch privilege as the background worker — fetches the image bytes itself, decodes them, and runs WebGPU inference, so the image data never leaves the browser and never crosses `chrome.runtime` messaging as a large binary payload (only URL strings and small JSON results are passed between contexts). ## Performance Measured on an Apple Silicon Mac via [`tools/verify.html`](tools/verify.html), which runs the shipped engine in the browser: | | per forward pass | | --- | ---: | | First inference (pipeline warm-up) | ~42 ms | | Steady state | **17-20 ms** | That is the full 384x384 ViT-Small/16 forward pass — 577 tokens, 12 encoder blocks, ~15.3 GMAC — in fp32, with no quantization. Weight load from the IndexedDB cache is a one-off ~90 ms. Because each image is scored at several views, wall-clock cost per image is roughly 20 ms times the number of views (2-4, depending on the image's size). For reference, the same model as a Q8-quantized ONNX graph on the WASM backend runs ~136-143 ms per image, so the hand-written WebGPU path is roughly 7x faster while keeping full fp32 precision. ### Where the speed comes from The forward pass is recorded once, at weight-load time, into a static list of (pipeline, bind group, workgroup counts); per-image work is then just "upload the image, replay the plan, read back one float". - The whole model runs as **one compute pass** rather than ~150 begin/end pairs. - Every buffer, uniform and bind group is allocated once at load, not per image (previously ~150 `createBuffer` + `createBindGroup` calls *per image*). - **One generic strided batched GEMM** replaces five separate kernels. It is register-tiled: a 16x16 workgroup produces a 64x64 output tile, 16 outputs per thread, so each shared-memory read feeds 4 MACs instead of 1. Shared tiles are k-major and padded to kill bank conflicts. - Bias, GELU and the residual add are **fused into the GEMM epilogue**, removing the separate elementwise passes and two activation buffers. - **Q, K and V are one fused GEMM**; strides read the Q/K/V slices back out, and read V transposed for the attention product, with no transpose pass. - The last encoder block runs its MLP on the CLS row only, and the final layernorm on one row instead of 577. Together these took the forward pass from ~67 ms to ~17 ms with **bit-identical output** — the engine still agrees with the CPU reference to every digit printed (see [Verification](#verification)). ## Verification [`tools/`](tools/) contains an offline oracle: `reference-forward.mjs` is a plain-JS fp32 CPU implementation of the same forward pass, with no dependencies. Both it and `verify.html` can be fed a fixed pseudo-random input, so the CPU and GPU paths see bit-identical tensors: | | P(fake) | | --- | --- | | CPU reference (Node) | `0.891299` | | WebGPU engine | `0.891299` | Re-run this after touching any kernel. See [`tools/README.md`](tools/README.md). ## Architecture ``` content script (finds elements, overlays badges) │ chrome.runtime messages (URL string only) ▼ background service worker (manages offscreen doc lifecycle, routes messages) │ chrome.runtime messages (URL string only) ▼ offscreen document (fetches image, WebGPU device, model cache, inference engine) ``` An offscreen document is used (rather than running WebGPU in the service worker or content script) because it's the standard MV3 pattern for DOM/GPU-dependent background work: it has a real `navigator.gpu` + canvas context and persists independently of any particular tab. ## Build & install No build step or bundler is required — the extension is plain ES modules loaded directly by Chrome. 1. Clone this repository. 2. Open `chrome://extensions` in Chrome. 3. Enable **Developer mode** (top-right toggle). 4. Click **Load unpacked** and select the `extension/` directory in this repo. 5. Visit any webpage. On the first image encountered, the extension downloads the model weights once (~87MB, progress shown in the extension popup) and caches them in IndexedDB. After that, everything runs offline. Requires a Chrome version with WebGPU support (Chrome 121+ recommended; `chrome://gpu` should show WebGPU as available). No other dependencies. ## Confidence scores & the 65% threshold Every analyzed image gets a badge showing a single number: the probability the image is AI-generated. Only the colour changes across the bounty's 65% threshold. - **Red**: `P(AI) >= 65%` — the image is also blurred, reversibly. Hovering reveals it temporarily; clicking reveals it for good. - **Yellow**: between 35% and 65%. - **Green**: `P(AI) <= 35%`. The displayed number is the calibrated score. The raw model output is kept in the badge tooltip and the console log for debugging. ## Privacy Image bytes are fetched by the extension (needed to run the model on them) but are never sent anywhere except through the local WebGPU pipeline running on your own GPU. No image, URL, or inference result is transmitted to any server controlled by this project or anyone else. ## Repository layout ``` extension/ manifest.json MV3 manifest src/ background.js service worker: routing, image fetch, offscreen lifecycle content.js finds images on the page, requests analysis, renders badges content.css badge styling offscreen.html/.js WebGPU device, model download/cache, inference popup.html/.js status popup lib/ model-config.js architecture + preprocessing constants safetensors.js minimal .safetensors parser model-cache.js IndexedDB weight cache preprocess.js resize/crop/normalize (CLIP-style), multi-scale views calibration.js maps the raw sigmoid onto the displayed threshold shaders.js WGSL compute kernels vit-engine.js forward-pass orchestration tools/ offline CPU oracle + in-browser verification harness ``` ## License MIT — see [LICENSE](LICENSE).