listing-tool v3 ยท Collectibles Intake Agent

Shelf photo in.
Listing draft out.

A local-first vision pipeline that looks at a shelf of designer vinyl, names every figure it can prove, prices them, and stages drafts for human review โ€” spending $0 on cloud AI until it genuinely has to.

shelf photoโ†’ detect + tileโ†’ CLIP embedโ†’ matchโ†’ confidence ladderโ†’ human reviewโ†’ Sales Hub draft
370
vetted reference images
45
golden test photos
0
confident-wrong, all-time
$0
cloud AI spend
Why it's built this way

Four rules, no exceptions.

Every design decision below falls out of these.

Local first, cheap first.

Apple's Vision framework finds the objects, an on-device CLIP model matches them, and Apple's on-device foundation model breaks ties. A metered Claude Haiku tier exists in the code but stays dormant without an API key โ€” and a hard ledger caps spend at $5/month even then.

Never confidently wrong.

A wrong answer must arrive wearing a low-confidence label. Every accept carries checkable evidence, and the human review step is structural โ€” drafts never publish themselves. Keeping confident-wrong at zero outranks raising the accuracy number.

No marketplace automation. Ever.

Reference images come only from compliant, read-only sources. When allvinyls.com's robots.txt forbade image fetching, the answer was a manual drop-folder with automatic labeling โ€” not a workaround.

Evidence over claims.

Builders โ€” human or AI โ€” don't get believed; exit codes, row counts, and diffs do. That discipline caught a leaked accuracy number and an env-var name inside generated HTML before either shipped.

Anatomy

The pipeline, stage by stage.

One photo becomes dozens of identified, priced, evidence-backed suggestions. Here's what happens in between.

1

Detect & tile

Apple Vision's saliency request is precise but stingy โ€” at most ~3 boxes per image, no matter how crowded the shelf. TiledVisionDetector fixes that: slice the photo into a grid, run saliency per tile, compose the results. The grid adapts to the photo (derive_adaptive_grid() targets ~420 px tiles): a 2268ร—4032 portrait becomes a 10ร—5 grid and 50โ€“100 crops; a 500 px web photo correctly yields a handful. This was the single biggest accuracy lever found โ€” pre-tiling, a full shelf produced 14โ€“17 crops and missed most figures.

Overlapping boxes are deduped (IoU > 0.5, higher score wins). Figures straddling a tile seam can duplicate โ€” a known artifact; review is the merge point.

2

Embed with on-device CLIP

Each crop is embedded with CLIP ViT-B/32 through Apple's Core AI stack โ€” a float16 .aimodel export driven by a compiled Swift CLI, invoked as a subprocess. 512-dimensional vectors, ~5 ms per embed, zero Python ML dependencies. (The PyTorch alternative needed a 14 GiB venv and lost the bake-off.)

3

Match with mean-top-2

Cosine similarity against every reference embedding, aggregated per figure as the mean of its two best images. This defeats style attractors โ€” a single real-photo reference among product shots that otherwise absorbs every real-photo query. Output: top_k_figures(k=5), a ranked candidate list.

4

Decide on the ladder

Thresholds are never hard-coded โ€” they come from data/tune_report.json, regenerated from the golden set every training turn. The report carries an agg field and the pipeline refuses one whose statistic doesn't match production; a mismatch once produced silently-wrong thresholds, so now it's a hard error.

5

Review, then push

Each run emits an interactive review.html plus intake.json. Approved results enter the running Sales Hub through its existing public seam โ€” POST /api/intake, then PUT /api/items/{id}, localhost only โ€” arriving as draft items with the full evidence trail in their notes. The draft โ†’ ready โ†’ exported state machine keeps a human between the agent and any marketplace.

Cheap โ†’ expensive, only as needed

The confidence ladder.

Every crop rides the ladder until something accepts it. Each tier costs more than the last; each accept records its method and confidence.

CLIP
$0 ยท ~5 ms
high  Accept if the top score clears the tuned threshold (currently 0.8274) and the margin over the runner-up clears the tuned margin. The margin rule catches colorway twins โ€” same sculpt, different paint, tiny margin โ†’ escalate.
Local VLM
$0 ยท seconds
medium  Apple's on-device foundation model via the fm CLI: shown the crop and the top-5 candidate thumbnails, asked to name one with visual evidence (colorway, base stamp). A per-crop failure is a miss, not a crash โ€” the beta daemon wedges under load and a ~370-call session must survive it. Names something outside the top-k? Confidence forced low, nothing chosen.
Haiku
metered ยท dormant
Claude Haiku, currently switched off (no API key). When live, every call must pass the ledger against the $5/month cap โ€” at the cap the crop stays unidentified rather than silently skipping the gate.
Human
the point
Unidentified and low-confidence crops land in review with their candidate lists intact. The reviewer's verdict outranks every tier above โ€” and becomes training data.
How it gets smarter

The training flywheel.

No gradient descent. The model is frozen โ€” what improves is the reference index and the thresholds, one scored batch at a time.

01PipelineRun each new shelf photo; crops and suggestions land per run.
02ScoreA human reviews the interactive page: right candidate, approve, note, label.
03ApplyConfirmed crops become new references or golden queries โ€” never both.
04Re-embedNew references get CLIP vectors; the index grows.
05Re-tuneThe golden set replays; fresh threshold and margin are written.
06MeasureCompare, bank the numbers, commit. Feed it more photos.
The leakage rule โ€” learned the hard way Adding the same confirmed crop to both the reference index and the golden set means the golden query later matches itself. The first flywheel turn read 93.1% top-1 when the honest, independent-photo signal was ~78%. Rule: per scored run, apply --add-refs or --add-golden, alternating between runs; a turn ledger records where the alternation stands. A leaked number is never reported as field accuracy.

One day of turns โ€” 2026-07-11

TurnWhat happenedRefsGoldenTop-1Caveat
0Adaptive tiling ships; catalog rebuilt + eBay gap-fill693977.8%honest baseline
1Runs 1โ€“6 scored (407 crops), both flags applied7132993.1%leaked
2Runs 7โ€“9 scored; alternation begins7303892.1%partially leaked
3The purge: 371 operator-marked bad eBay refs quarantined3654488.6%honest drop โ€” index now trustworthy
4Runs 19โ€“22 scored; runs 23โ€“46 produced (web photos)37045โ€”thr 0.8274 ยท awaiting audit

The purge is the day's most important event: the eBay enrichment that once supplied most of the index turned out to be ~80% junk โ€” wrong figures on real listings. The accuracy number fell when it left; the accuracy meaning rose. The binding constraint now is coverage: 193 figures have no reference image at all, and the cure is exactly this loop plus the drop-folder.

Reference

Scripts & usage.

Everything runs from the repo root with one environment block. The pinned agent venv is the only correct interpreter โ€” the app's venv deliberately lacks the agent's dependencies, and vice versa.

# environment โ€” every session, verbatim
cd ~/Developer/projects/listing-tool
export PYTHONPATH=$PWD \
  AGENT_COREAI_CLI=$PWD/agent/probes/bakeoff/coreai/coreai_bakeoff/.build/out/Products/Release/coreai_clip_cli \
  AGENT_COREAI_MODEL=/Volumes/1TBEDRIVE/Workingspace/coreai-models/exports/clip-vit-base-patch32_float16_static.aimodel \
  AGENT_VISION_DETECT_CLI=$PWD/agent/probes/vision_detect_cli
PY=/Volumes/1TBEDRIVE/Workingspace/agent-venv/bin/python

agent.intake.pipeline โ€” photo in, suggestions out

The main event. Writes crops, DB rows, and suggestions.json under data/catalog/intake/<run_id>/.

$PY -m agent.intake.pipeline <photo> --tune-report data/tune_report.json \
    --db-path data/catalog.db --run-notes "context"
# --grid 8x7 / --target-tile-px N override adaptive tiling (never 0)
# --dry-run prints the crop/tier plan without any model calls

agent.handoff.run โ€” review artifacts, optional push

Generates the interactive review.html + intake.json. --push sends all approved results of that run into the live app as drafts.

$PY -m agent.handoff.run <run_id> [--push] [--app-url http://127.0.0.1:8000]

agent.handoff.apply_decisions โ€” the flywheel step

Applies a scored decisions JSON: overrides become manual/high, rejects clear the choice, confirms stand. All-or-nothing validation, atomic replacement. One growth flag per run, alternating.

$PY -m agent.handoff.apply_decisions <run_id> <decisions.json> --add-refs   # or --add-golden
# then verify counts grew: user refs + golden must both be โ‰ฅ last turn

agent.matching.tune โ€” thresholds from the golden set

Replays every labeled photo in data/golden_ids/ against the index. Copy the numbers into data/tune_report.json with "agg": "mean_top2" โ€” the pipeline rejects a mismatched statistic.

$PY -m agent.matching.tune --golden data/golden_ids --backend coreai --db data/catalog.db

agent.probes.build_audit_site โ€” remote audit package

Builds a self-contained static site (all images inlined) for remote scoring โ€” always with --dataset so both audit surfaces ship.

$PY -m agent.probes.build_audit_site --runs 23,24,โ€ฆ --dataset --out data/audit_site_vN
# then: grep -rqE "/Users|/Volumes|ANTHROPIC|sk-|Bearer|API_KEY" out/ โ†’ must find nothing

agent.probes.apply_corrections_purge โ€” dataset cleanup

Consumes the dataset page's corrections export. Quarantine, not delete: files move aside, embeddings and rows go. Dry-run by default; the destructive step needs operator approval.

$PY -m agent.probes.apply_corrections_purge <corrections.json> [--apply]

agent.probes.import_allvinyls_drop โ€” compliant catalog growth

allvinyls.com forbids automated image fetches, so the operator saves images manually (keep original filenames) into the drop folder. The importer auto-labels each file by parsing the cached series-page HTML โ€” filename โ†’ figure, no manual tagging.

$PY -m agent.probes.import_allvinyls_drop <drop-dir> [--apply]   # dry-run first
Humans in the loop, at scale

Audit sites.

Scoring, packaged for anyone with a browser โ€” deploy the folder to Cloudflare Pages or open it straight from file://.

Run pages

Every crop with its top-5 candidates as radio buttons, an approve checkbox, a free note, and three label fields โ€” figure, series, artist โ€” for items the catalog doesn't know yet. Export downloads a decisions JSON that apply_decisions ingests unchanged (labels pack into the note as label: figure=โ€ฆ|series=โ€ฆ|artist=โ€ฆ).

Dataset page

Every reference image as a thumbnail with a "bad" checkbox, grouped by figure with source labels, plus the "needs images" list of figures with no reference at all. Export produces the corrections format the purge tool consumes.

Before anything deploys Generated files must contain no absolute paths and no key material. The build sanitizes evidence strings โ€” one build genuinely leaked the literal string ANTHROPIC_API_KEY from a ladder-evidence line โ€” and the final gate is a manual grep for /Users, /Volumes, and key patterns, run pipe-free so the exit code is real.
Where things live

Data layout.

PathWhat it is
data/catalog.dbSQLite: series, figures, reference images, embeddings, intake runs/results. Gitignored but persistent โ€” never in tmp.
data/catalog/images/<fid>/Reference files per figure; confirmed crops arrive as user_crop_<run>_<result>.jpg.
data/golden_ids/The measurement set: labeled real photos named <figure_id>__qN.jpg.
data/catalog/intake/<run>/Per-run crops, suggestions.json, review.html, intake.json.
data/tune_report.jsonThe live thresholds: {n, threshold, mean_margin, agg}.
data/catalog/images_quarantine/Purged refs (reversible), named <fid>__<original>.
agent/.envSecrets, env-only, never committed: eBay API creds, dormant Anthropic key slot.
/Volumes/1TBEDRIVE/Workingspace/The heavy stuff: pinned agent venv, Core AI model exports, drop folders.
Paid for in hours

Gotchas & dead ends.

Operational gotchas

Exit codes die in pipes โ€” cmd > out 2>&1; echo $?, never cmd | tail; this shipped a false "tests pass" twice.

/private/tmp is wiped on restart; durable state lives only in repo data/ or the external drive.

The fm daemon wedges after heavy batches โ€” restart the Mac; the ladder degrades safely meanwhile.

exFAT worktrees show every file modified until core.fileMode false.

Small web photos (~500 px) legitimately yield 2โ€“8 crops โ€” adaptive tiling working, not breaking.

Measured dead ends โ€” don't retry

negative  Dark-background ref augmentation โ€” compositing refs onto black to match dark shelves. No gain, cleanly reverted.

negative  Query-side dark matting โ€” actively hurt dark figures.

negative  Self-consistency quarantine @ 0.72 โ€” caught nothing; the "attractors" were genuinely correct photos. The real fix was mean-top-2 aggregation.

As of 2026-07-11, end of day

Where it stands.

MetricValue
Reference index370 images โ€” every one operator-vetted or original-dataset (48 from confirmed crops)
Golden set45 labeled real photos
Live thresholds0.8274 accept ยท 0.0815 margin ยท mean-top-2
Confident-wrong, all-time0
Figures needing images193 โ€” the binding constraint
Awaitingdecisions for runs 23โ€“46 ยท next dataset-audit corrections ยท ~75 unprocessed photos

The path from here is more of the same loop: score the outstanding runs, refill the 193 imageless figures via the drop-folder, and keep alternating refs and golden until the tune numbers turn boringly stable. The machinery is done โ€” from now on, the work is feeding it.