A technical walkthrough of “Which Celebrity Vegetable Are You?” — private WebAI inference with Google’s LiteRT.js, multi-crop classification, a 387-item world produce catalog, and fingerprint-stable matching that still feels dynamic at party scale.
TL;DR
We built a browser-only app that:
- Loads Google’s LiteRT.js Wasm runtime
- Compiles real
.tflitemodels (MobileNet ImageNet + optional food classifier) - Runs multi-crop inference on WebGPU / Wasm / WebNN
- Ranks a 387-entry global produce catalog (not 24 fixed jokes)
- Picks a winner with a photo fingerprint–weighted top pool so many users diverge
- Builds unique card copy from templates + seed
- Exports a share card with compile + inference latency (+ crop count)
No photo upload server. No face ID. Real classification on your pixels.
Live stack: Vite · TypeScript · @litertjs/core · optional @litertjs/tfjs-interop
Why LiteRT.js (and why produce)?
LiteRT.js is Google’s production-minded WebAI runtime: Wasm/XNNPack, WebGPU, WebNN, .tflite interchange, and TF.js interop.
A dry MobileNet page doesn’t travel. A privacy-first produce oracle does — especially when hundreds of outcomes and photo-stable variety make a group demo feel alive.
Architecture
┌─────────────────────────── Browser ───────────────────────────┐
│ UI (main.ts) │
│ │ │
│ ▼ │
│ LiteRT engine │
│ loadLiteRt → loadAndCompile(.tflite) │
│ 5 crops → model.run × N → average softmax → top-25 │
│ │ │
│ ▼ │
│ Produce oracle │
│ score 387 catalog entries (labels + color + fingerprint) │
│ weighted pick among top ~18 → template persona card │
│ │ │
│ ▼ │
│ Result UI + share PNG (latency · crops · catalog size) │
└───────────────────────────────────────────────────────────────┘
| Module | Role |
|---|---|
litert/engine.ts |
LiteRT load/compile/run, multi-crop, optional TF.js interop |
litert/preprocess.ts |
Crops, NCHW/NHWC, ImageNet norm, top-K / averaged top-K |
personas/produce-catalog.ts |
Compact world produce list (keywords, region, color) |
personas/roster.ts |
Deterministic moniker/tagline/trait templates |
personas/score.ts |
Ranking + fingerprint-weighted selection |
share/card.ts |
Magazine-style PNG |
Inference path (still 100% LiteRT for the model)
await loadLiteRt('/wasm/');
const model = await loadAndCompile('/models/mobilenet_v2.tflite', {
accelerator: 'webgpu', // or wasm / webnn
});
// multi-crop → Tensor → model.run (repeated) → average probs → top-25
const run = await runModel(bundle, imageSource, 'webgpu', false, true);
// run.preds, run.timings.crops === 5
Default path uses LiteRT Tensor + model.run. Optional UI toggle uses runWithTfjsTensors for the same compiled LiteRT model with TF.js boundary tensors.
From logits to a vegetable (not random)
- Features from the photo — multi-crop classification + canvas color stats + byte fingerprint.
- Score every catalog entry — keyword overlap on a wide top-K, name-token boost, color/brightness soft match.
- Take top ~18, then weighted sample using the fingerprint (higher scores more likely; same photo ⇒ same pick).
- Build persona — templates seeded by fingerprint ⊕ produce id so monikers/taglines differ per person/photo.
- Show alts — second/third aura vegetables for a richer card.
Same photo + WebGPU vs Wasm ⇒ same produce (trust). Different photos ⇒ different ranks and seeds (diversity).
Why 387 entries beat 24 personas
With two dozen outcomes, parties collapse into “everyone is kale.” A global produce catalog (chayote, ube, fiddlehead, shishito, samphire, …) plus a soft top-pool pick spreads results without abandoning the image.
Card text is procedural, so even two “tomato” wins can read differently.
Privacy
Models and Wasm download as static assets. Pixels are not uploaded for inference. Disclaimer: parody, not biometrics.
Setup
npm install # postinstall: Wasm + .tflite
npm run dev
What’s next
- Drop-in custom
.tflite+ labels - Produce-specialized model for sharper plant/food classes
- Optional aggregate “popular vegetables this week” (counts only, no images)
References
Code & more: https://www.dailybuild.xyz/project/189-veggie-oracle
Top comments (0)