0023 — Tile & inspector data (score chip, burst crown, capture)¶
- Status: IMPLEMENTED (2026-07-16) — split (§6) and open questions (§7) RESOLVED:
Q1 → profile-relative mapping, Q2 → JPEG + RAW hand-rolled
pkg/exif(no dep), Q3 → Composition stays AI-review-only. Slice A (score + crown) building now; Slice B (capture/EXIF) is the fast-follow. - Why now: the
0022"Pro Dark" redesign shipped three surfaces with the data behind them stubbed, because the values weren't exposed: the tile score chip (number), the best-of-burst crown, and the inspector CAPTURE block.0022§6 deferred all three here so the redesign stayed presentation-only. This spec surfaces the data to light them up. - Depends on:
0022(the surfaces),0007(the cull signals),0004(burst clustering),0014§2.1 (frameView/analysis cache),0020(per-frame reasons + faces already surfaced the pattern this follows). - Non-goals: no new cull judgement (thresholds/gates unchanged,
0006); no new ML; presentation-facing derivation only. Not a re-score of the shoot.
1. What we found (the data mostly already exists)¶
- Per-frame signals are persisted.
analysis.Signals[frame](cull.Signals) holdsSharpness,ClippedHighlights,ClippedShadows,MeanLuma,FaceCount,MinEyeOpen,MinSmile,MinFacing, andAesthetic(0..1, or -1 unscored). The analysis cache is already loaded inlistFrames(and the dims backfill from0022now guarantees it's populated for legacy shoots too). - Best-of-burst is derivable. Cluster membership lives on each
FrameVerdict.Cluster; the keeper is the cluster's kept frame (repickCluster,clusterKeep). So a per-framebestflag is a derivation over verdicts, not new state. - Capture metadata is NOT extracted. No ISO/shutter/aperture/focal/lens anywhere.
pkg/raw/raw.gowalks the EXIF IFD but only to locate the embedded preview — it reads none of the capture tags. JPEG EXIF (APP1) isn't parsed at all.
So score + crown are a derivation (no schema change, no new deps); capture is a new extraction (new tag parsing, possibly a dependency). That asymmetry drives the split.
2. Slice A — score chip + sub-scores + burst crown (derivation only)¶
Add to frameView (all computed in toFrameView from data already loaded; nothing new
persisted):
score int— an overall 0–100 quality score for the chip number.subScores []{key,label,value}— Focus / Exposure / Eyes / Emotion (0–100), for the inspector's "cull scores" rail (the deterministic complement to the opt-in AI-review dimensions already shown there).best bool— true when this frame is its burst's keeper → the tile's gold crown.
The tile chip renders score + rating pips; the burst marker becomes the gold crown when
best; the inspector rail shows the sub-score bars (backed by real signals, no longer a
stub). R-REDESIGN-2 holds — none of this uses the gold accent to carry verdict meaning.
Score mapping (the open question, Q1). The signals are raw (Laplacian variance, clip fractions, probabilities) on different scales. The proposed mapping resolves each signal relative to the active cull profile's own thresholds (so the score agrees with the verdict the profile gave, and re-tunes with it), then aggregates:
- Focus = ramp
Sharpnessacross the profile's reject→maybe focus thresholds → 0–100. - Exposure = 100 − penalty(
ClippedHighlights,ClippedShadowsvs their max gates). - Eyes =
MinEyeOpen×100 whenFaceCount>0, else n/a (omit the bar). - Emotion =
MinSmile×100 when assessed, else n/a. - Overall
score=Aesthetic×100 when a scorer ran; otherwise the weighted mean of the available sub-scores. (Composition has no deterministic signal — it stays an AI-review-only dimension, not a cull sub-score.)
The mapping is pure and profile-driven (R-GLOBAL-7), unit-tested with golden signals.
3. Slice B — capture / EXIF block (new extraction)¶
Surface capture {iso, shutter, aperture, focalLength, lens, camera} on frameView,
read from the frame's EXIF, cached in the analysis pass (like dims — extract-once). The
inspector CAPTURE block renders it.
- JPEG: parse the APP1 EXIF segment for the capture tags.
- RAW (CR2/NEF/ARW/DNG): extend
raw.go's existing EXIF-IFD walk to read the capture tags it currently skips (ISO 0x8827, ExposureTime 0x829A, FNumber 0x829D, FocalLength 0x920A, Model 0x0110, LensModel 0xA434), decoding rationals. - Provider-neutral
Capturestruct inpkg/shoot; a smallpkg/exifreader behind it. WASM-safe (pure Go byte parsing, no cgo).
4. Requirements¶
- R-DATA-1
score/subScores/bestare derived read-only from the analysis cache + verdicts; no new persisted verdict state, no schema migration. - R-DATA-2 The score mapping is pure and profile-relative; golden-signal unit tests
pin it, and it never feeds back into the cull verdict (display-only,
0009-style). - R-DATA-3 Capture is extracted once and cached; a frame with no/unreadable EXIF simply omits the block (never errors the frame list).
- R-DATA-4 The EXIF reader is cgo-free / WASM-compilable (
0001§12.1). - R-DATA-5 No cull judgement change; verdicts/reasons are byte-identical before/after.
5. Testing¶
- Golden-signal table → score/sub-scores (pure,
t.Parallel()). - Burst fixture → exactly one
bestper cluster (the keeper). - EXIF fixtures: a JPEG with known tags and a small RAW header → parsed capture; a tag-less image → omitted block. Non-destructive assertions unchanged.
6. The split (recommended)¶
- Slice A — IMPLEMENTED 2026-07-16.
cull.Score(profile-relative, golden-tested) feedsscore/scores/bestontoframeView; the tile shows the score number + gold burst crown, and the loupe a Cull scores rail (Overall + Focus/Exposure/ Eyes/Emotion). Verified live: 556/556 scored, 143 crowns. No schema/deps. - Slice B — IMPLEMENTED 2026-07-16. A hand-rolled, dependency-free
pkg/exifreads camera/lens/ISO/shutter/aperture/focal from JPEG APP1 and TIFF/RAW IFDs (WASM-safe, golden-tested). The studio backfills it from a bounded 512 KiB prefix of each original, caches it inanalysis.Capture, and servesframeView.capture; the loupe renders the CAPTURE block. Verified live: 556/556 JPEGs parsed (SONY ILCE-7M4, lens, ISO, etc).
7. Open questions¶
- Q1 — score mapping (gating Slice A). Adopt the profile-relative mapping in §2? Or
prefer a simpler "surface
Aestheticas the score, raw signals as sub-bars" (less meaningful when no aesthetic scorer ran), or defer the overall number and show only the sub-score bars? Recommendation: the §2 profile-relative mapping. - Q2 — capture scope (Slice B). JPEG-only first (covers the current test shoot), or
JPEG + RAW together (Hailey shoots RAW, so real value needs it)? Vendor a JPEG EXIF lib,
or hand-roll the ~6 tags to stay dependency-free and share the RAW path? Recommendation:
hand-roll a tiny
pkg/exiffor both, no dependency. - Q3 — composition. Leave Composition as an AI-review-only dimension (no cull signal), as proposed? Or drop it from the rail entirely for consistency?