0036 — Phase 4: learning Hailey's taste¶
- Status: PARKED (2026-07-27, Matt) — deliberately, pending real usage. The decision is not that Phase 4 is unwanted; it is that the questions in §7 cannot be answered honestly from where we stand. There are zero human corrections on disk (§2), so any objective we picked today would be a guess dressed as a design. krites needs bedding-in time on real shoots first.
- Re-entry criteria — revisit when any of these is true:
- A few hundred genuine corrections exist across more than one shoot (one wedding is one sample — §5 L4).
- Or the maybe rate is understood: §2.1's cheap hypothesis — that 68% "maybe" is a mis-tuned profile, not a missing model — has been tested by hand. If tuning halves it, this phase changes shape entirely.
- Or the objective (§7-Q1) becomes obvious from using the tool, which is the likeliest way it gets settled.
- ⚠️ What must NOT wait: §9. Parking the learner is safe. Parking the substrate fixes is not — several defects in §3.3 actively destroy signal as it is generated (a burst re-pick erases the machine's pick; a soft reset deletes agreement; a re-cull discards the fresh proposal). Every shoot culled between now and re-entry either accrues usable evidence or silently does not. The waiting only pays off if the recording is fixed first.
- This spec deliberately does not propose a design. Its job is to make "learns her taste" mean something specific enough to build. §7 is the point of the document; everything before it is the evidence needed to answer it.
- Owner: —
- Realises:
0001§11 Phase 4 — "the feature that turns 'a good culler' into her culler". The last unbuilt phase. - Depends on:
0005(decision capture — the substrate),0007-DEC-1(the deferred aesthetic model),0009(review capture),0012(the config cascade the learner would write into),0028(the job engine a training pass would run on).
1. Why this spec is question-shaped¶
Every other phase arrived with its meaning settled. "Cull the unusable" and "remove that exit sign" describe themselves. "Learns her taste" does not — it names a feeling, not a behaviour, and at least five different products would satisfy the phrase.
It is also the phase most likely to be quietly wrong. A culler that misjudges sharpness is debuggable; a learner that drifts toward mediocrity is not, because its output looks like an opinion. Aftershoot sells this as the headline feature, which raises the stakes on getting the definition right rather than shipping something that merely qualifies as machine learning.
So: no recommendation here until §7 is answered. What follows is the ground truth.
2. The uncomfortable starting position¶
Measured on lyndsey-denis, the real 556-frame shoot, 2026-07-27:
| Frames | 556 |
Verdicts where verdict != proposed (human corrections) |
0 |
Frames with an override flag |
0 |
| Edit records | 0 |
| Export directory | does not exist |
| AI reviews captured | 12 |
aesthetic signal |
-1 on every frame (seam inert) |
There is no training data. Not "not much" — none. Every verdict on disk is still the machine's own proposal, untouched by a human.
This is not a gap to be filled by waiting, either. 0005 anticipated exactly this — "if Phase 4
shipped tomorrow it would start from zero history" — and built the substrate early so that Phase 4
would begin with "a year of real labelled examples". That bet has not paid out, because the shoot
has been culled repeatedly for engineering validation and never actually reviewed as a
photographer would review it.
Consequence: any design premised on mining an existing override history is designing against data that does not exist. The cold-start problem is not an edge case to handle later; it is the starting condition, and it may be the whole first slice.
2.1 The other number, which may matter more¶
krites currently defers on more than two-thirds of the shoot. "Maybe" means "a human must look at this" — so on this evidence krites removes about a third of the work, not the "fraction of the time" the product promises.
That reframes the goal. The valuable thing may not be matching her taste in the abstract but collapsing the maybe pile — converting frames she would have confidently kept or rejected into confident verdicts. Those are different objectives with different designs, and §7-Q1 asks which one we are actually building.
It also suggests a cheaper hypothesis worth eliminating first: the maybe rate may simply be a
mis-tuned profile, not a missing model. 377 frames demoted largely by blink and burst rules is
what an untuned wedding-default looks like. If a manual tuning pass halves it, that reorders the
entire phase.
3. What we already have to learn from¶
3.1 The features (dense, real, already cached)¶
Per frame in .krites/analysis.yaml (pkg/shoot/shoot.go:222), keyed by frame:
cull.Signals(pkg/cull/cull.go:39) —sharpness,clipped_highlights,clipped_shadows,mean_luma,face_count,min_eye_open,min_smile,min_facing,min_valence,aesthetic.- Per-face boxes — normalised
x,y,w,h,confidence,eye_open,smile,frown,valence,facing. Per-subject, not just per-frame — richer than the reduced signals. - Perceptual hash (burst structure), dims, and EXIF capture — camera, lens, ISO, shutter, aperture, focal length.
This is a genuinely usable feature vector, and it is already there for every frame including
rejects — which 0007 §8-Q3 specifically required, because a learner needs the full distribution.
3.2 The labels (designed correctly, currently empty)¶
FrameVerdict (pkg/shoot/shoot.go:54) carries Proposed (machine), Verdict (effective),
Override (human touched it), DecidedAt, Rating (stars — human-only, and currently consumed
by nothing). preserveOverrides restores human corrections across re-culls, so a correction pair
survives arbitrarily many re-analyses.
The design is right. It is simply unexercised.
3.3 What the substrate promised but does not deliver¶
The explorers found several places where the record is thinner or more fragile than the specs imply. These are not design choices to respect — they are defects to fix before, or as part of, any learning work:
| Problem | Consequence for a learner | Where |
|---|---|---|
preserveOverrides freezes Proposed at the first override — a re-cull restores the whole stored record, discarding the new machine proposal |
You can never observe "the machine now agrees with her". Learning progress is unmeasurable — which also breaks any "we agree 94%" claim | pkg/pipeline/pipeline.go:271 |
Override conflates a verdict change with a rating change |
A star-only edit looks like a correction with Proposed == Verdict — a phantom no-op label |
pkg/studio/actions.go:340 |
| No profile reference on the verdict record | After any re-tune you cannot tell which thresholds were in force when a decision was made. 0005 §2 lists profile id as part of a decision; it is stored once per shoot, latest cull only |
pkg/shoot/shoot.go:214 |
| — | pkg/shoot/reset.go:68 |
|
analysis.yaml is overwritten every cull; AnalysisVersion has moved 8 times |
Historic feature values are unrecoverable, and old ones mean something different. Labels outlive the features that explain them | pkg/shoot/shoot.go:209 |
Burst re-pick overwrites Cluster on every member and sets no DecidedAt |
The machine's original burst pick is erased by the human's — the single most interesting comparison in the whole dataset, destroyed at the moment it is made | pkg/studio/endpoints.go:679 |
Reasons persist as prose, not cull.Issue kinds |
The learner must regex "eyes closed (23% open, below 40%)" to recover a structured cause that existed in memory |
pkg/cull/cull.go:164 |
| Review structured data is dropped — only Markdown is written | 0009 §5 promised "a structured record in the analysis cache". Scores, viability band and comparative reasoning survive only as ## composition — 7/10 · strong headings |
pkg/studio/review.go:145 |
| No export ledger | The strongest possible signal — "she actually delivered this frame" — is only a directory listing. No time, no options, no frame set | pkg/export/export.go:43 |
| Edit records carry no human provenance — and there is no human edit surface for straighten/crop/look at all | 0006 R-EDIT-4 stores the machine's Basis so overrides can be related to proposals, but nothing records that a human changed anything. Today the only human input in the edits tree is a removal region |
pkg/develop/edit.go:16 |
exif.Capture has no capture timestamp |
No true temporal ordering; bursts are grouped by filename order | pkg/exif/exif.go:19 |
Profile.Fingerprint() omits 4 of 12 knobs — SmileSoft, FacingSoft, FrownSoft, AestheticFloor |
Two materially different profiles hash identically. A learner that tunes those knobs cannot tell its own experiments apart | pkg/cull/profile.go:47 |
That last one is a live bug regardless of Phase 4 and should be fixed on its own merits.
4. What the judgement actually looks like today¶
Worth stating plainly, because it bounds what "learning" can even mean.
cull.Resolve (pkg/cull/cull.go:185) is a two-phase short-circuit with no scoring and no
weights:
if hard := hardIssues(s, p); len(hard) > 0 { return Reject }
if soft := softIssues(s, p); len(soft) > 0 { return Maybe }
return Keep
- 4 hard gates (→ reject): min sharpness, clipped highlights, clipped shadows, eye-open hard.
- 6 soft floors (→ maybe): soft sharpness, eye-open soft, smile, facing, frown, aesthetic.
- 1 clustering knob (dedup distance) and 1 face-qualification filter (min face box).
- A fourth demotion path outside
Resolve: burst non-winners are demotedkeep → maybe, with the winner chosen by a hardcoded priority chain — eyes ▸ smile ▸ facing ▸ aesthetic ▸ sharpness (pkg/pipeline/pipeline.go:549).
Three things follow:
- Any hard gate fires first and hides everything else. A frame rejected for blown highlights never reports that it was also soft. The reason list is therefore not a full account of why a frame lost — a subtlety a learner training on reasons would get wrong.
- There is no notion of a frame being good — only of it not tripping anything. Nothing can currently express "this frame is exceptional", which is precisely what taste is.
- The burst tiebreak order encodes a taste judgement in Go, hardcoded — that eyes matter more
than smile, which matters more than sharpness. Hailey may disagree, and today she cannot say so.
This is arguably the single most learnable thing in the system, and it is the one place
0001§6's "never hardcode the judgement" rule is already broken.
4.1 One very convenient fact¶
Threshold values are not part of cache invalidation (pkg/pipeline/pipeline.go:178). Re-judging
uses cached signals with no decode and no model pass, so re-culling 4,000 frames against new
thresholds is near-instant.
That makes a threshold-fitting learner cheap to build and cheap to evaluate — you can score a candidate profile against her whole history in milliseconds, and try thousands.
The exceptions matter: MinFaceBox filters upstream of the cache, and enabling the aesthetic
scorer invalidates every cache by construction (aestheticEnabled is part of the signal context).
So L1 below is nearly free and L3 costs a full re-analysis of every shoot.
5. Hypotheses — five things "learns her taste" could mean¶
A ladder, roughly by ambition. These are deliberately drawn as alternatives to argue with, not a plan. They are not mutually exclusive, but they have very different costs and failure modes.
L0 — Learn nothing; just report agreement¶
Measure and surface how often krites agreed with her. "krites matched you on 94% of frames; where it didn't, it was usually too harsh on blinks."
- Learns: nothing.
- Needs: the correction pairs; the
preserveOverridesfix (§3.3) so "now agrees" is observable. - Acts by: telling her, and telling us which knob is wrong.
- Why it might be the right first move: it is the only option that works with a handful of
labels, it is honest, it builds the trust the later phases spend, and it produces the diagnosis
that tells us whether L1–L4 are needed at all.
0019§7 flags it as a marketing gap that is "cheap given0005already captures the data". - Risk: it isn't learning, and if the answer is "94%" it may reveal the phase isn't needed — which is a useful result but not the exciting one.
L1 — Fit the existing thresholds¶
Treat it as a 12-parameter optimisation: find the profile that best reproduces her decisions.
- Learns: where her thresholds actually sit versus
wedding-default. - Needs: maybe 200–500 corrections. Evaluation is near-free (§4.1).
- Acts by: proposing
cull.profile.*changes through the existing config cascade — exactly the mechanism0001§6 already promises ("the Phase 4 learning loop feeds adjustments back into her profile"). - Strengths: fully explainable ("you kept 23 frames I rejected for softness; your real floor is
38, not 50"), reversible, deterministic, respects
R-GLOBAL-7because the output is config, and requires no new model, no ONNX, no cache invalidation. - Limits: can only move axes we already measure. It can never learn "she loves a backlit silhouette" because nothing measures that.
- Honest framing: this is calibration, not taste. It may still be most of the value.
L2 — Learn how the signals combine¶
Replace the fixed hard/soft short-circuit with a learned function over the 10 signals — weights, or a small tree.
- Learns: trade-offs. That she forgives softness when valence is high; that a blink in a group shot matters less than in a portrait.
- Acts by: a learned scoring layer feeding the verdict.
- Tension — this is the one that fights the architecture.
R-GLOBAL-7andR-CULL-4require verdict resolution to be pure over(signals, profile). A learned combiner is only compatible if the weights are themselves config — data flowing through the existing seam, not state insideResolve. That is doable, but it changesProfilefrom a set of thresholds into a model, and it costs the thing0020fought for: legible reasons. "Rejected: score 0.42" is not an explanation, and explainability is a shipped, deliberate feature. - Open: can we keep per-signal reasons under a learned combiner, or does L2 trade away explainability? If it does, is that trade acceptable?
L3 — A personal model over embeddings¶
Run CLIP (or similar) per frame, keep the embedding, train a small head on her keep/reject history.
- Learns: things we do not measure and cannot name — composition, moment, "her look".
- This is already half-decided.
0007-DEC-1deferred the aesthetic model to Phase 4 and chose its shape: not NIMA, but CLIP + aesthetic head, explicitly because "the learner needs a per-frame embedding (score and embedding in one pass)". TheScorerseam exists, is nil-safe, and its three consumers (verdict floor, burst tiebreak, display score) are wired and inert. - Costs: ~350 MB model, ~200–400 ms/frame, and invalidates every analysis cache — a full
re-analysis of every shoot ever culled. Also the WASM question (
0001§13-Q7) gets sharper. - Constraint that does not move:
R-ASC-2/0006R-AES-2 — aesthetic may rank and demote, never hard-reject. A personal model cannot be given the authority to bin a frame. - Risk, and it is the real one: with a few hundred labels a model this size learns the photographer's lighting conditions, not their taste. It will look like it works — that is what makes it dangerous. Needs held-out shoots, not held-out frames.
L4 — Learn context, not just taste¶
Learn that a dim reception and a bright ceremony want different thresholds, and propose per-shoot overrides rather than one global profile.
- Fits the architecture unusually well:
0012already gives a global base and sparse per-shoot overrides, and describes them as "per-shoot taste [that] can legitimately differ between, say, a dim reception and a bright ceremony". - Needs: shoot-level features (which exist — EXIF, luma distribution) and enough shoots to generalise, which is a much scarcer resource than frames. One wedding is one sample.
L5 (orthogonal) — Learn the burst tiebreak¶
Not on the ladder; separable and possibly the best value-per-effort in the whole phase.
The burst winner is chosen by a hardcoded priority chain (§4). Every re-pick she makes is a direct statement that the chain is wrong for her. It is a small, well-posed learning problem — learn the ranking function — over the clearest labels in the system, and bursts are where the maybe pile is concentrated.
Blocked today: the re-pick overwrites the machine's own pick (§3.3), so the comparison is destroyed at the moment it is created. Fixing that is cheap and worth doing whatever else we decide.
6. How it could act — the trust question¶
Independent of what is learned, and possibly more important.
0001 §11 says krites proposes adjustments. That word is doing a lot of work and should be
tested rather than inherited:
| Posture | Shape | Cost |
|---|---|---|
| Report only | "Here is where we disagreed." | No agency, no risk. L0. |
| Propose, she approves | A diff: "raise the softness floor 50 → 38? This would have changed 23 of your last 400 decisions." | Auditable, reversible, matches "the human stays the final arbiter" (0001 §1). Costs her attention every time. |
| Apply, announce, allow undo | Adapts, then tells her what changed and why. | Feels magical; but a silent drift she didn't notice is exactly the failure that destroys trust in a culler. |
| Apply silently | Just gets better. | The product fantasy. Unfalsifiable from her seat, and un-debuggable from ours. |
The 0019 #26 "second-guess / fatigue fighter" idea is a genuinely different fifth option worth
weighing on its own: don't change the profile at all — notice in the moment that she is rejecting
frames she historically keeps, and ask. "You usually love shots like this. Tired, or reconsidering?"
That treats the learned model as a check on her, not a replacement for her — which may be both
the most useful and the most defensible form of "learns your taste", and it degrades gracefully when
the model is weak.
7. The open questions¶
This is the part that needs answering before any code. Recommendations are offered where the evidence points somewhere, and explicitly withheld where it does not.
Q1 — What is the objective?¶
Match her decisions? Collapse the 68% maybe pile? Surface frames she'd otherwise miss? Reduce time-to-cull? These pull in different directions: a learner optimising agreement is rewarded for saying "maybe" forever, since a maybe is never wrong. No recommendation — this is the product question, and everything else follows from it.
Q2 — What counts as evidence of taste?¶
Verdict overrides only? Also star ratings (captured, consumed by nothing)? Burst re-picks? Which frames she exported (currently unrecorded)? Which she edited? Time spent on a frame? Lean: overrides + burst re-picks + exports. Exports are the least ambiguous statement of "I want this" in the entire system, and we currently throw them away.
Q3 — Is "she left it alone" a signal?¶
The overwhelming majority of frames will never be touched. Is that agreement, or is it "she never looked"? Today they are indistinguishable — and soft reset deletes exactly these records. This is a genuine fork. If untouched-means-agreement, the dataset is 556 labels; if not, it is however many she explicitly touched, which today is 0. We may need to capture "reviewed" separately from "changed".
Q4 — Propose or apply? (§6)¶
Lean: propose, at least initially — but the fatigue-fighter framing may be a better product than either, and deserves consideration on its own rather than as a footnote.
Q5 — What do we do about cold start?¶
0019 §7 identifies this and proposes a "rate these 50 frames" onboarding wizard — but §9 of that
same spec schedules it into Phase 1, not Phase 4. That contradiction is unresolved and this spec
should either absorb the wizard or explicitly leave it.
Worth noting: a calibration wizard is useful independent of any learner, and might be the fastest
way to find out whether the maybe pile is a tuning problem (§2.1).
Q6 — Global, per-shoot, or both? (L4)¶
Which learnings generalise across weddings and which are properly per-shoot? Getting this wrong means either a profile that thrashes between shoots or one that never adapts to a dark church.
Q7 — Does the aesthetic model land in this phase?¶
It is the difference between a config-tuning feature and an ML feature: ~350 MB, ~200–400 ms/frame,
and a full re-analysis of every shoot. 0007-DEC-1 already chose its shape (CLIP + head).
Lean: not in the first slice. L0/L1 need no model, and their results tell us whether L3's cost is
justified. But note the sequencing trap — every day we defer it, we accumulate history whose
embeddings we do not have, and backfilling means re-analysing everything anyway.
Q8 — Latest-state or an event log?¶
0005 §7-Q2 explicitly deferred this decision to Phase 4: "event log deferred to Phase 4 if the
learner wants trajectories". Today everything is latest-state and re-culls overwrite.
Lean: an append-only decision log is the single highest-value substrate change (§3.3), and it is
cheap. It is also the only way to answer "is krites getting better?"
Q9 — What does "agreement" mean with three verdicts?¶
Is machine-maybe → human-keep a disagreement, or the system working as designed? Any honest
accuracy claim (0019 §7) needs this defined, and it is easy to define flatteringly.
Q10 — Does learning touch develop/looks, or only the cull?¶
0001 §11 says "cull-profile/look adjustments". But there is no human edit surface for
straighten/crop/look at all (§3.3), so there is nothing to learn from — a prerequisite feature,
not a learning feature.
Lean: cull only, and say so.
Q11 — How is a learned change reversed?¶
R-ND-3 requires everything reversible. If a proposal is accepted and she hates the result three
shoots later, what does undo look like? Config history? A pinned "known good" profile?
Q12 — Is learn exposed over MCP?¶
0005 §5 already reserves the command name. 0009 gated review off MCP because it egresses
images; a local learner does not, but it does mutate her config.
Q13 — How do we know it works?¶
The failure mode is a model that looks right and is wrong. Held-out shoots (not frames), since frames within a wedding are wildly correlated. What result would make us abandon the phase?
8. Constraints that are not up for discussion¶
Settled elsewhere; a design that violates one of these is wrong regardless of merit.
- Originals immutable; everything reversible;
analysis/regenerable —R-ND-1..4. A trained model or aggregate must not live in the disposable analysis cache. - No catalogue. Records stay per-shoot in
.krites/; cross-shoot learning is a train-time fan-in over shoot directories the user points at —0005R-CAP-½. krites owns no global store. - Local-first. Training runs on her machine. Cloud-side learning is not pre-authorised —
0005§5 says revisit only if it ever moves cloud-side, i.e. a new decision. - Verdict resolution stays pure over
(signals, profile)—R-GLOBAL-7,R-CULL-4. Anything learned enters through config or as a new deterministic signal, never as hidden state. - The engine stays cgo-free/WASM-compilable —
0001§12.1. Any CLIP/ONNX work lives in its own adapter package, never imported bycull/shoot/quality/dedup/xmp. - Aesthetic never hard-rejects (
R-ASC-2); reviews never affect verdicts (R-REV-6); nothing learned is written to XMP (R-AES-4). - Not multi-tenant. Single user, local.
0019§8-2 is explicit that SaaS must not distort near-term sequencing. - Config-driven, never hardcoded —
0001§6. (Which the burst tiebreak currently violates.)
9. Prerequisites, whatever we choose¶
These are worth doing on their own merits, and every hypothesis in §5 depends on some of them:
- Fix
Profile.Fingerprint()to cover all 12 knobs — a live bug today (§3.3). - Stop
preserveOverridesdiscarding the fresh proposal — without this, "is it improving?" is unanswerable. - Record the machine's burst pick alongside the human's, and stamp
DecidedAton re-picks. - Separate "verdict changed" from "rating set" in
Override. - Persist structured review records —
0009§5 promised this and it was never implemented. - An export ledger — what was exported, when, from which verdicts.
- Persist
cull.Issuekinds alongside the prose reasons. Stop soft reset deleting untouched verdicts— withdrawn, see §3.3. What remains is the real question: capture "reviewed" separately from "changed" (Q3), so that "she looked and agreed" becomes a label instead of being indistinguishable from silence. That needs a decision, not a fix.
Items 1–4 are small. Items 5–8 are the difference between having a dataset in a year and not.
10. A deliberately unambitious strawman¶
Offered only as something to react to.
- Slice 0 — make the data real. §9 prerequisites, plus: actually cull and review a shoot properly, end to end, as a photographer. We cannot design this phase against a dataset of zero. This slice is also the one that tests §2.1 — whether the maybe pile is a tuning problem.
- Slice 1 — L0, the agreement report. No learning. Tells her how well krites is doing and tells us which knobs are wrong. Ships value with a handful of labels.
- Slice 2 — L1, threshold fitting, proposal-only. Nearly free to evaluate (§4.1), fully explainable, uses the config cascade that already exists.
- Slice 3 — decide from evidence. With a real dataset and a measured agreement rate, choose between L2, L3 and L5. Not before.
The bias here is deliberate: every slice must produce something useful even if the next one is never built, because this is the phase most likely to prove harder or less valuable than it sounds.
11. How to use this document¶
Parked 2026-07-27. The next step is not code, and it is not §7 either — it is use. The questions in §7 are answerable from experience with real shoots and are not answerable without it.
When the re-entry criteria in the status block are met, this spec gets rewritten as an actual design
with a slice plan and moves PARKED → IN PROGRESS. Until then it stands as the record of what we
knew, what we had, and what we would have had to guess.
The one thing that should not wait is §9 — see the status block.