Mutual Exclusions
Only one tag in each pair can apply to a photo. The higher-confidence tag wins.
←→
Required Combinations
If a tag applies, automatically also apply another tag.
IF
→ also apply
Confidence Thresholds
Minimum confidence Claude must return for a tag to be applied.
Override for
Plugin Activity
Idle
·
0 calls
·
last:
Time Filename Result Dur
No activity yet — run "Tag Selected Photos with AI" from Lightroom
01   The Lightroom Plugin
What the plugin does

The plugin lives inside Adobe Lightroom Classic. It automates keyword tagging of sports photos using Claude AI — with human feedback that makes it smarter over time.

01
AI tagging in one click — Select photos, hit Tag with AI. Each photo is sent to Claude with game context (opponent, jersey edition, roster) and gets keyword tags back.
02
Writes to LR catalog — Tags become native Lightroom keywords, instantly visible in the keyword panel and ready to filter and export.
03
Give Feedback dialog — Review each AI decision: ↑ Keep, ✗ Remove, or + Add a missed tag. Decisions write to LR and train the gallery on the server.
04
Sync from Web — Reviews done on photoai.work (any device) are pulled into Lightroom with a single menu click.
CLIP ViT-B/32 ONNX claude-sonnet-4-6 6 positive examples / tag 4 negative examples / tag ~1 API call / photo
Lightroom Classic — Library Menu
Game Context…
Tag Selected Photos with AI
Give Feedback…
Sync from Web
Manage Roster…
02   photoai.work
The web platform

The cloud layer that makes the plugin smarter over time — gallery training, web review queue, and R2-backed persistence that survives server restarts.

🖼
Gallery Training
Every feedback decision adds the photo as a positive or negative example for that tag. The AI sees these examples on the next tagging run — it improves with every session.
🔍
Web Review Queue
Tagged photos appear here for review on any device. Approve or reject on a phone courtside — decisions queue server-side and sync to LR on next pull.
R2 Persistence
Gallery examples, tag definitions, rules, keyword-edit queues, and feedback decisions are all backed up to Cloudflare R2 — Railway can redeploy without losing state.
03   Integration Flow
How it all connects

Two primary workflows. Branch A is the shoot-to-review loop inside Lightroom. Branch B covers every sync path between the web platform and the LR catalog.

NetsAI Lightroom Plugin
BasketballTagger.lrplugin · photoai.work · Railway · Cloudflare R2
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
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
B — Web Sync
Three paths · same contract: keep LR ↔ photoai.work in sync
↓ Pull Down
Sync from Web
Sync from Web in LR menu
GET /api/keyword-edits
?consume=1
← { edits: [{ filename,
  tag, action }] }
atomically dequeues
R2-backed queue
Tags.normalize()
createKeyword()
✓ Keywords added /
removed in LR
↑ Push Up
Web Review → LR
Thumbs-up/down on
photoai.work/status
any device
POST /api/keyword-edits → { filename, tag,
  action: add|remove }
← queued in R2
_keyword_edits
+ _feedback_decisions
survives restarts
Run ↓ Pull Down
in LR to apply
✓ Keywords applied
to LR catalog
⏪ Backfill
All-time history
Sync Reviewed → LR
button on photoai.work
POST /api/sync-approved ← { queued: N }
Reads all-time
_feedback_decisions
→ rebuilds queue
deduplicates per
filename + tag
Run ↓ Pull Down
in LR to apply
all N edits
✓ All historical
reviews applied
to LR catalog