A — Shoot & Tag → Review
Tag Photos with AI · Give Feedback
Select photos in LR grid → "Tag Selected Photos with AI"
Export JPEG · base64-encode · 2 concurrent workers
CLIP ViT-B/32
Pre-screener · No API call
Runs server-side via ONNX — scores all 42 tags by cosine similarity between the photo embedding and gallery examples. Tags with ≥3 gallery examples use visual similarity; tags with fewer fall back to text description embeddings.
Phase 1 — top-5 by normalized score. Phase 2 — add best tag from any unrepresented semantic category (score ≥ 0.40 floor), ensuring each visual group has a voice.
42 tags scored
5–11 candidates
gallery path · text path
6 semantic categories
claude-sonnet-4-6
Single call · Full gallery
One call per photo — Sonnet sees the full gallery (6 pos + 4 neg) for every candidate, plus tag definitions, jersey context, roster, and rules. Replaces the old 5–8 call bucket pipeline entirely.
Output: { reasoning, keywords, confidence }
6 positive ex / candidate
4 negative ex / candidate
1 call / photo
rules.json · Rules Engine
Confidence thresholds filter low-certainty tags. Required combinations fire (e.g. Driving → Look). Mutual exclusions enforced — higher-confidence tag wins.
claude-haiku-4-5
Conditional · Mutex re-query
Only fires when the rules engine detects a mutual exclusion conflict (e.g. dunk + block on the same photo). Focused 2-tag call with no mutual exclusion rules — lets Haiku adjudicate the specific contested play without interference.
fires ~10% of photos
2 tags · focused context
POST
/api/tag-photo · /api/session-upload
→ { dataUrl, filename, gameContext }
← { keywords, pendingReview, reasoning }
✓ Confirmed keywords written to LR catalog · checkpoint saved
Give Feedback… in LR menu · card UI per photo (up to 50)
↑ Keep
✗ Remove
+ Add missed
POST
async
/api/feedback
↑ { tag, polarity:"pos" } ✗ { tag, polarity:"neg" } + { tag, type:"missed" }
← photo added to gallery training examples · saved to R2
✓ Single withWriteAccessDo after Done · Keywords applied to LR · Gallery updated