# Evaluation Results
## ✅ SHIPPED MODEL (2026-08-13): re-fit head, pooled BA @0.65 = 0.8915
The Flickr false-positive investigation ended in a head re-fit on frozen CommFor
features (`refit_head.py`, λ=0.1 pull toward the original head, train data disjoint
from all eval sets). Old vs new, identical calibration protocol:
| Metric | Original head | **Refit head (shipped)** |
|---|---|---|
| Pooled balanced acc @0.65 | 78.4% | **89.2%** |
| AUC | 0.858 | **0.955** |
| Fake recall (OpenFake test) | 78.0% | **86.5%** |
| FPR: OpenFake reals | 13.0% | **7.0%** |
| FPR: COCO val | 7.5% | **7.0%** |
| FPR: VOC 2012 | 1.5% | **0.5%** |
| FPR: Commons quality (modern pro) | 28.3% | **10.0%** |
| FPR: Unsplash (modern pro) | 42.6% | **12.9%** |
| OOD CommFor fakes flagged (must stay high) | 100.0% | **99.6%** |
| OOD CommFor reals FPR | 5.3% | **2.7%** |
- Calibration offset: **+2.29** (baked into engine.js); model
`commfor_vits_384_refit.onnx` installed as extension `detector.onnx`
(ONNX↔features parity 4.8e-7; browser↔Python decision parity 16/16, max Δlogit 1.24).
- The 2022-Flickr sets now flag at ~21–23% — consistent with genuine AI contamination
in those "real" scrapes, not model error (provably-real modern sets sit at 10–13%).
- Training reals: Unsplash Lite (pre-2022, camera-EXIF) 900 + Wikimedia Commons QI 900
+ OpenFake val 700; fakes: OpenFake val 927 (generator-capped). COCO/VOC train adds
were dropped (HF stream timeouts) — worth adding in a future round.
*2026-08-13 — proxy benchmark: OpenFake core/test, 400 real + 400 fake, web-realistic
re-encode (≤1024px, JPEG q85) applied identically to both classes. Split 530 dev / 270
holdout. Fakes span 19 generators incl. flux-2-klein, midjourney-7, gpt-image-1.5/2,
sora-2, z-image-turbo, nano-banana-pro, seedream-v5 (2025–26 era).*
## Headline
| Model | AUC (dev) | Best BA (dev) | **BA @0.65 on holdout, dev-calibrated** |
|---|---|---|---|
| commfor ViT-S 224 | 0.768 | 0.697 | — (dropped) |
| **commfor ViT-S 384** | **0.904** | **0.834** | **0.8115** ✅ (bar: 0.75) |
| 224+384 mean-logit ensemble | — | 0.817 | — (worse than 384 solo) |
Calibration: logit offset **+7.6897** (dev-measured) moves the optimal operating point to
the bounty's fixed 0.65 threshold. On holdout the post-calibration optimum lands at
threshold 0.52 (≈centered) — the calibration transfers.
Robustness: dev BA stays 81–83% within ±1 logit of the chosen operating point (not a
knife-edge optimum).
## Per-generator fake detection (224 model, dev, @best-t) — why 224 was dropped
Weakest: flux-2-klein 27%, midjourney-7 37%, z-image-turbo 48%, gpt-image-1.5 50%.
Strongest: illustrious 92%, lumina 100%, wan-video 82%. The 384 model recovers most of
the frontier-generator gap (fake acc 84% overall at its operating point).
## Quantization
- fp32 ONNX: parity with PyTorch (max logit diff 8e-6). 87MB — shipping this.
- Dynamic int8: **broken** for this ViT (prob diffs up to 0.7 on real images, decision
flips). Do not ship. Static QDQ quantization with calibration data is the follow-up.
## Browser parity & live test (2026-08-13)
- **Parity (extension/test/parity.html)**: same 16 files through engine.js (canvas) vs
harness.py (PIL). Decisions at the calibrated 0.65 threshold: **16/16 match**.
Max |Δlogit| 1.98, most <0.7; large diffs sit far from the boundary.
drawImage smoothing beats createImageBitmap resize for PIL-parity (1.98 vs 3.04).
- **Live pipeline (extension/test/demo.html)**: real content.js + engine on a 32-post
feed. 16 unique images → exactly 16 inferences (URL dedupe works; duplicate posts
free). 7/8 flagged images were fakes; the 1 real flagged is a genuine model FP
(Python flags it identically — not a browser divergence). Sequential queue: one
inference in flight, ~810ms/image on single-thread WASM (WebGPU unavailable in the
embedded test pane; expect much faster in real Chrome).
- **Viewport priority & no re-processing** (`?scroll=9000`): loading mid-feed analyzed
the two on-screen images first (#14, #13) — not the document-top images. Scrolling
down analyzed exactly 2 new images; scrolling **back up re-analyzed 0**. Only visible
images are ever processed.
- **Badge placement**: every badge host aligned pixel-exact to an image, and every
badge had confidence ≥0.65. Visually confirmed: the AI sample carries a red
"AI generated" tag in its top-left corner; the real photo carries nothing.
## Graphic/screenshot gate (2026-08-13)
Live X testing surfaced noisy, inconsistent scores on **non-photographic content**
(charts 58% vs 78%, tables 55% vs 80%, a flat promo graphic at 96%): out-of-distribution
inputs for a photo-vs-generated model. Fix: a pixel-statistics gate (flat-run fraction +
palette size, thresholds tuned in `graphic_gate.py`) fires on 100% of rendered
charts/tables/UI/text pages vs 1.5% of photos / 1.7% of fakes. Gated images get a
-3.0 logit prior penalty and a distinct "graphic" chip in the UI.
- Benchmark impact: pooled BA 0.8915 → 0.8909 (-0.06pp, noise); real acc +0.13pp.
- Verified in the real extension (e2e-graphics): 6/6 graphics → "graphic" chips at
1–6%, AI photo still 100% red pill, real photo 11% chip.
- Model parity unaffected (rawLogit path): 16/16 decisions vs Python.
## Discovery coverage (extension/test/hardcases.html)
Every pattern below holds the same AI image, so a miss is a discovery failure rather
than a model disagreement. All pass after the discovery rewrite:
| Case | Where it shows up in the wild | Result |
|---|---|---|
| plain `
` | everywhere | tagged |
| `` + srcset | responsive images | tagged |
| CSS `background-image` | x.com, grok.com cards/avatars | tagged |
| lazy `src` swapped in later | infinite feeds | tagged |
| open shadow root | web-component UIs | tagged |
| `blob:` URL | generated-image viewers (grok) | tagged |
6 cases required only **2 inferences** — the URL cache collapsed the five sharing a URL.
Also fixed for real sites: content-script injection into already-open tabs (SPAs never
reload), `all_frames` for iframes, CSSOM styling instead of a `