WARD/INDEX·SPEC
MUSER / PERSONAL
GITHUB ↗·CONTACT
← MUSER
LOCAL-FIRST2026 · SUB-TOOL OF MUSER · $0 CORE

MUSERPERSONAL

A hidden, fully-isolated sub-tool that triages a 46,978-image Google Photos Takeout into personal, in-between, and reference — locally, offline, $0 for the core. The architecture, the math that decides each photo, and the engineering that keeps the labels safe.

A self-portrait from the personal corpus, scored by the classifier. Personal · 0.75
Corpus46,978 IMG · 33K FACES
StackSigLIP-2 · LanceDB
Isolation~/.muser-personal
Cloudgpt-4o-mini · band
01/

Problem

undifferentiated · 46k images

A decade of Takeout is an undifferentiated 46k-image dump — real life mixed with saved internet art, game caps, screenshots, scanned reference, movie stills.

You want the personal stuff kept and private, the genuinely-aesthetic stuff promotable to the main Muser library, and the junk gone — without hand-sorting 46k files or shipping them to a cloud. So the whole core runs locally, offline, for $0.

02/

Pipeline

one photo · left → right
1
Ingest
Parse the Takeout JSON sidecar → capture time, GPS, people tags, roll, cam (EXIF). $0.
2
Embed + facets
SigLIP vector → LanceDB; faces, doc/screenshot, aesthetic, color. $0.
3
Score → P
Two scorers fuse evidence into a personal-ness score P∈[0,1]. $0.
4
Bucket
P routes to personal / in-between / reference. $0.
5
VLM triage
Only the uncertain middle → gpt-4o-mini; count + dollar cost shown before spend. $.
6
Curate + export
Name people · learned delete model · move/copy into the main library. $0.
03/

The scorer

two brains · sup decides 100%

Most descriptions stop at the heuristic. In production there are two scorers, and on the live corpus the second decides 100% of buckets (sig.sup == true on all 46,978).

① Cold-start heuristic

_classify_one — a hand-weighted fusion of 5 signals + 3 floors. Interpretable, needs zero labels, fully explainable.

~59% CV · personalness.py:171

② Supervised override

reclassify_supervised — a logistic regression P = σ(w·e + b) over the frozen SigLIP embedding, trained on your labels. Once you label, it re-buckets everything and wins.

~86% CV · personalness.py:619

Why two: a frozen-embedding linear probe beats both the hand-tuned heuristic and full fine-tuning when labels are scarce — ~769 parameters can't overfit a few hundred labels, and it reuses the embedding you already computed. The heuristic is the interpretable bootstrap; the probe is the accurate decider. The 59 → 86% jump is why it exists.

# evidence-driven: personal-ness needs POSITIVE evidence, not "doesn't look aesthetic"
base = 1 − R                            # R = reference-likeness (linear probe)
P    = 0.45·F + 0.40·cam + 0.15·base   # F=faces · cam=EXIF · base=appearance
if doc:    P = max(P, 0.50)             # utility → reviewable, never auto-personal
if cam:    P = max(P, 0.48)             # camera-taken → at least reviewable
if people: P = max(P, 0.95)             # user-TAGGED person → decisive
P = clip(P ± 0.08, 0, 1)               # album nudge (camera-roll vs named album)
bucket: P≥0.55 personal · P≤0.40 reference · else in-between
Why these shapes. The killer early bug: 1−R alone (“doesn’t look like the aesthetic library”) tagged saved internet art as personal — it’s non-aesthetic, not personal. Fix: demand positive evidence. Faces carry the most weight (0.45), appearance is a faint 0.15 tiebreak, and a tagged person — the one signal a human explicitly authored — is a hard 0.95 floor. The floors are a priority ordering (tagged ≫ camera ≫ doc), not weights.
04/

Signals

5 signals · 3 floors · 3 buckets
SignalweightWhat it iscoverage
F faces×0.45max(recurring person ≥12-cluster, face-box ≥18% of frame)33,036
cam EXIF×0.40 +floorcamera Make/Model present = “a photo I took”2,931 / 46,978
base 1−R×0.15inverse of a linear probe’s reference-likenessall
people taggedfloor .95you tagged this person in Google Photosdecisive
doc utilityfloor .50screenshot / scan / receiptreviewable
A album±0.08camera-roll vs named album nudge
BucketbandMeaning
personalP ≥ 0.55keep — your life
in-between0.40–0.55review — ambiguous / camera reference / docs
referenceP ≤ 0.40eligible to move into the main aesthetic library
Aesthetic score Q: shelved. Q once demoted high-aesthetic personal photos to in-between. Empirically dead — Q=0.5 (default) on 100% of entries, and the supervised model ignores it. Worse, it conflated two orthogonal axes: a gorgeous photo of someone you love is more personal, not less. Removed as a bucket determinant; kept recoverable.
05/

Real cases

actual photos · actual signals
selfie
SelfieF=1.0 R=.24
heur .48 in-betw
model .75 personal
tagged
Taggedpeople=T
floor .95
personal
reference
ReferenceR=.02 F=.01
P .07
reference
camera
Cameracam=T
P .80
personal
screenshot
Docdoc=T cam=T
P .90
personal

Live verdicts from the supervised model. The selfie shows the heuristic→model gap that motivates the probe: the formula says in-between, the trained model — from the embedding alone — correctly says personal.

06/

After

curate · cleanup · export
Faces → People
InsightFace embeds every face → HDBSCAN clusters → you name/merge (path-anchored, survives re-cluster). A named person force-buckets their photos personal, reversibly.
Cleanup
Junk score (Laplacian-variance blur, exposure, tiny-res, dupes) + a learned P(delete) logreg on your 🗑/keep flags. Trash via /usr/bin/trash (recoverable); embedding snapshotted at tag-time so it stays a training positive.
Export
Confirmed reference is MOVED to the main library; in-between is COPIED. A manifest + saved vectors keep moved images as training examples — exporting never drops them from the model.
06.5/

Visualization Suite

interactive diagrams of the pipeline

Explore the decision logic with interactive visualizations: the evidence flow through the pipeline, the embedding manifold, and the decision boundaries. Each diagram is interactive — hover to reveal signals, click to explore details.

View Visualizations →
07/

Engineering

what makes it trustworthy
Isolation
The personal process re-execs itself with MUSER_HOME set before any heavy import, so import order can never leak the main root. Write endpoints 403 on the main instance.
Labels are gold
A corrupt label file once read silently as {}, and the next save clobbered ~1,800 human labels from empty. Fix: corrupt-but-present → quarantine + raise, never silent {}. Back-ported to every sidecar + per-process temp names + atomic writes.
No data loss
Moved (reference→main) and trashed (delete) images both keep their embedding — via manifest+npz link and a tag-time snapshot — so curation never silently shrinks the training set.
Verified
Every number on this page was pulled from the live system: real signal values from personalness.json, real verdicts from the running service.