0009 — AI image review: the Critic capability¶
Status: IN PROGRESS (2026-07-05) — an on-demand, provider-backed image-critique
capability, separate from the cull. Slice 1 (the seam + rubric + sidecar + CLI
against a fake, incl. the availability probe) is landed and inert; the Gemini
backend is next (§7). Companion to
0001-krites.md (§5 providers, §13-Q3 cloud, R-PRIV-*),
0003-studio.md (the studio surface + privacy indicator),
0005-decision-capture.md (the learning substrate a
review feeds), and 0006-develop.md / the cull profile pattern
(config-driven catalogs, never hardcoded).
This is a DRAFT for review. Per
CLAUDE.mdstep 0, no implementation begins until the open questions in §10 are resolved or deferred. Decisions already taken with Matt (2026-07-04) are recorded inline.Requested by Hailey. She already uses Gemini as a separate tool to review selected frames — not to cull, but to learn about her own style and technique and find ways to improve composition and craft. This folds that workflow into krites: select one or more frames, press Review, and an AI returns a comprehensive analysis to inform both an image's viability and her growth as a photographer.
1. The shape of the work — and how it differs from cull¶
The cull is deterministic + local ML: fast, per-frame, objective signals
(blur, exposure, eyes, dedup, expression) turned into a verdict by a profile. It
judges thousands of frames to triage a shoot. Review is the opposite in every
axis: on-demand (never automatic), generative (a multimodal LLM writing
prose + structure), slow and considered (seconds per frame, a paid call), run
on a handful of hand-picked frames, and learning-first — its primary job is
to teach, with a viability read as a secondary output. It never affects a cull
verdict (R-REV-6): a review is advisory, stored beside the frame, and offered
to the Phase-4 learner.
Decision (2026-07-04, Matt) — role: both, learning-weighted. The review delivers a viability read and rich learning feedback, with the coaching as the emphasis. Output and storage are designed around the learning value first.
2. The Critic provider seam¶
A new provider capability in the 0001 §5 mould — the sixth seam alongside
Decoder, FaceAnalyzer, AestheticScorer, Clusterer, Inpainter:
// Critic produces a structured, prose-rich critique of one or more frames against
// a rubric. Provider-neutral: a cloud multimodal LLM (Gemini, Claude) or a local
// VLM satisfies the same contract (0001 §5).
type Critic interface {
// Review analyses the images together against the rubric and returns the
// critique. Honours ctx (R-GLOBAL-8 timeout).
Review(ctx context.Context, images []Image, rubric Rubric) (Review, error)
// Probe reports whether review can run right now: the provider is configured
// (a key is present) AND reachable (network up, endpoint responding). It
// returns an Availability whose Reason explains a not-ready state for the UI
// (R-REV-10). Cheap, side-effect-free, honours ctx.
Probe(ctx context.Context) Availability
}
- Backends (decision 2026-07-04, Matt): Gemini + Claude now, and design for a
local VLM. Both cloud multimodal models sit behind the one seam; the request is
provider-neutral so a future on-device VLM (e.g. a quantised Qwen-VL/LLaVA-
class model through the
0004/0008ONNX- or llama.cpp-style runtime) slots in without call-site changes and brings review back on-machine. Model ids/pricing for the Claude backend are pinned against the currentclaude-apireference at build time (latest Opus-class multimodal). - No default — off until a key is supplied (decision 2026-07-04, Matt). There is no default provider and no ambient enablement: review is inert until Hailey chooses a provider and provides its API token through the studio. There is no "just works" cloud path — the deliberate act of adding a key is the opt-in. Credential handling is the §3.1 contract.
- Injected, faked in tests — a fake
Critic(returns a canned structured review) drives every unit/e2e test of the command, storage and studio path, so the whole feature is testable with no network and no key (the0007/0008ship-inert discipline). - Timeout + cost — every call carries a deadline (
R-GLOBAL-8) and surfaces the token/estimated cost (R-REV-7), since these are paid calls. - Availability probe (
R-REV-10, decision 2026-07-05, Matt) — review is only offered when it can actually run. AProbereports readiness across two conditions: a provider is configured (a key is present for the chosen provider) and it is reachable (a network connection exists and the provider endpoint responds). When either fails, the review surfaces are disabled, not hidden, with a tooltip naming the reason (no provider configured / offline / provider unreachable) so Hailey knows why and how to fix it. With no backend at all (slice 1), the probe reports "no provider configured" — the inert state. The probe is cheap and side-effect-free (it never egresses an image), so the studio can poll it to keep the affordance live as connectivity changes.
3. Cloud-first — the honest privacy inversion (R-PRIV-*)¶
Review is the first genuinely cloud-first feature: the full image leaves the machine and is sent to a third-party model. That inverts krites' local-first spine, so the design must be maximally honest about it:
- Off by default; opt-in per capability (
0001§13-Q3). Enabling review is a deliberate choice Hailey makes. - Disclosure is mandatory (
R-PRIV-2): the command and the studio state, before sending, that the selected images will be uploaded to the named provider. The studio's persistent privacy indicator (R-UI-19) names the operation and the destination while a review is in flight. - Consent granularity (
R-REV-8, decision 2026-07-04): config opt-in + a first-use confirm. Choosing a provider + supplying a key is the opt-in; the first review shows a confirmation disclosing the egress; subsequent reviews rely on the persistent privacy indicator + a per-run image count, no repeated dialog. - Keys never touch config files or logs (
R-PRIV-3); the storage + transport contract is §3.1. - The local VLM path (design-for) is the privacy-preserving endgame — review without egress — which is why the seam matters.
3.1 Credential handling (decision 2026-07-04, Matt — R-REV-9)¶
The provider API token is the one genuinely sensitive secret this feature adds. Its lifecycle:
- Entered in the studio, never on the CLI. Hailey selects a provider and pastes its key into the studio settings. There is no env var, no config-file key, no ambient credential — the backend uses explicitly the stored key for the chosen provider and nothing else.
- UI→backend over the loopback only, behind the shared studio auth. The key
travels from the SPA to the local server across the studio's localhost/loopback
bind and no further (
R-UI), on an endpoint behind GTB'shttp.AuthMiddleware(cookie + bearer,pkg/authn) — the same studio-API auth0010§2.1/§6-Q1b/c introduces (and which requires the GTB ≥v0.27.2 bump). The transport is confined to the loopback interface — never a routable address. (If krites ever goes multi-tenant, this becomes TLS with a proper auth model — the middleware'sWithMTLSVerifieris the seam — explicitly out of scope now.) - Stored via GTB's keychain backend — local-studio only. The backend persists the key using go-tool-base's keychain credential backend, used only when the studio runs locally (the single-user Mac). We do not scope a custom credential backend for a future multi-tenant/hosted mode here; that is a deliberate later exercise.
- Used explicitly, scoped to the provider. The
Criticadapter is handed the resolved key for its provider at construction; it never reads process env or falls back to other provider config. The key is redacted from all logs/telemetry.
4. The rubric catalog (config-driven, R-REV-3)¶
What the critic assesses and how it speaks is config, never hardcoded — a
rubric catalog exactly like the cull-profile and look catalogs (0006 §6,
R-CAT-1). A rubric names the dimensions, the persona/depth, and the viability
framing. krites init seeds a starter wedding-review rubric Hailey tunes;
the Phase-4 loop can propose refinements from what she found useful.
Default dimensions (all tunable): composition (framing, balance, thirds, leading lines, negative space), exposure & technical (focus, sharpness, dynamic range, noise), light & colour (quality, direction, white balance, grade), moment & emotion (expression, storytelling, timing), styling & posing, and an overall viability read. Each rubric also carries a coaching directive (the learning-weighted persona: "explain why, name the craft principle, give one concrete thing to try next").
5. Output & storage¶
- Structured + prose (
R-REV-2, decision 2026-07-04): each dimension carries a numeric score, a qualitative label, and prose (assessment + one concrete thing to try), plus an overall viability band. The number/label feed the learner and drive compact UI; the prose is what teaches. (The numeric score is a review signal only — it never becomes a cull rating,R-REV-6.) - Per-frame sidecar (
R-REV-4, non-destructiveR-ND-*): stored as.krites/reviews/<frame>.md(human-readable) + a structured record in the analysis cache. It references the rubric id + provider + model + timestamp for provenance. Never written to XMP or the image — it is krites-internal. - Batch = comparative + per-image (decision 2026-07-04, Matt): a review over a selection produces a per-frame critique and, when >1 frame, a comparative pass ("of these six, #3 has the strongest light because…") — the comparison is where a lot of the learning lives (best-of-set reasoning).
- Feeds Phase-4 learning (
R-REV-5,0005): the reviews + Hailey's keep/ reject/override history are a rich substrate for learning her taste and, later, a personalised model — the headline Phase-4 goal.
6. Surfaces¶
- CLI:
krites review <frame…> [--rubric <id>] [--provider gemini|claude] [--compare]— the scriptable/low-level surface. - Studio: a Review action on one or more selected frames in the cull-review
grid → a disclosure confirm → progress (with the privacy indicator) → the
critique rendered inline (structured cards + prose), saved to the sidecar and
re-openable. This is the primary surface (matching
0001's studio-first stance). The Review action is gated by the availability probe (R-REV-10): when no provider is configured, the machine is offline, or the provider is unreachable, the action is disabled with a tooltip stating the reason, rather than failing on click. The studio polls the probe so the affordance tracks connectivity. - MCP (decision 2026-07-04): gated OFF MCP (
setup.ExcludeFromMCP, the export/remove pattern). Review egresses images, so an agent must never be able to trigger it — it is CLI + studio only (R-MCP-2extended to cover egress, not just pixel production).
7. Implementation plan (layered, the 0007/0008 discipline)¶
- Slice 1 — the seam + storage + CLI against a fake. ✅ Landed 2026-07-05. The
Criticinterface (incl.Probe), theReview/Rubric/Availabilitytypes (pkg/review), the seededwedding-reviewrubric (a code default, like the cull-profile/look seeds — a config-driven multi-rubric catalog loader is a later addition), the per-frame sidecar (shoot.WriteReview/ReadReview→.krites/reviews/<frame>.md), andkrites reviewwired throughcriticprovider.Buildto a faked critic, gated off MCP.review.Runprobes before any egress, so an unconfigured/offline critic declines with its reason; the probe reports "no provider configured" while inert. The studio wiring of the disable-with-tooltip affordance lands in slice 3. Unit-tested (engine, storage, command wiring with a fake) + an e2e feature (features/review.feature) pinning validation, the off-provider decline and the non-destructive guarantee. Inert without a backend. - Slice 2 — the backend, via GTB
pkg/chatmultimodal. ✅ Landed 2026-07-05 (on go-tool-base v0.28.0, which carries the chat multimodal input from [go-tool-base !191]). TheCriticis backed by achat.ChatClient(pkg/review/chatcritic):Reviewbuilds the rubric prompt, attaches each frame as achat.Media, and callsAskwith a JSONResponseSchema(chat.GenerateSchema[critiqueOutput]), then maps the structured result intoreview.Review(incl. tokenUsage). The provider is a config choice resolved throughchat, so Gemini and Claude both work with no per-provider adapter — this subsumes the old slice 4.Probekeeps our own contract (key configured + a cheap host-reachability dial,R-REV-10).criticproviderresolves the keychain key and constructs the critic; the bespoke provider HTTP client is gone. Unit-tested with a fakechat.ChatClient(the mapping) + an env-gated integration test run green against real Gemini (5 dimensions, viability, token usage). - Slice 3 — the studio Review UI. ✅ Landed 2026-07-05 (
0003). A ✦ AI review action on the loupe, gated by the availability probe (disabled with the probe reason as its tooltip when no provider is configured / offline / unreachable,R-REV-10); a first-use disclosure confirm before the image egresses (R-PRIV-2); the critique rendered inline (per-dimension cards + viability + token count) and saved to the sidecar. Studio endpoints:GET /api/v1/review/availability(probe),POST …/frames/{frame}/review(run + save), andGET|PUT /api/v1/settings/review(provider/model in config, the API key write-only into the keychain, never returned). TheCriticfactory is aServeroption, faked in tests. httptest-covered (gating, review flow, save, off-provider 501, offline 503); verified on the real authenticated server (cookie bootstrap → the gated availability/settings JSON). The keychain happy-path needs a working OS keyring (Hailey's Mac), so it is exercised there, not on the keyring-less CI box. - Slice 4 — (subsumed into slice 2). Claude is a
review.provider: claudeconfig flip through the samechat-backed adapter — no separate adapter. OpenAI likewise if ever wanted. - Slice 5 — comparative batch (best-of-set reasoning).
- Additive later — a local VLM backend (review without egress).
2.1 Backend via GTB pkg/chat (revised 2026-07-05)¶
Decision (Matt): don't hand-roll provider HTTP clients in krites. The GTB
pkg/chat client now takes multimodal input (images/PDF) across Gemini, Claude
and OpenAI behind one interface, with credential resolution, structured output
(Ask + ResponseSchema), usage accounting and snapshot persistence — exactly the
machinery a bespoke adapter would duplicate. So the Critic is a thin wrapper over
a chat.ChatClient. Gated on a go-tool-base release carrying !191 and a krites
GTB bump (the current pin is v0.27.2, which predates it).
Execution checklist (run when the release is out):
go get gitlab.com/phpboyscout/go-tool-base@<new release>;just ci.- Drop
pkg/review/gemini/(the bespoke adapter + its tests) entirely. - Add a
chat-backedCritic(ininternal/criticprovideror a smallpkg/review/chatcritic): constructchat.New(ctx, props, chat.Config{Provider, Token: <keychain key>, ResponseSchema: GenerateSchema[critiqueOutput]()});Review= build the rubric prompt +[]chat.Mediafrom the frames +Askinto acritiqueOutput, then map toreview.Review(port the field mapping from the oldgemini/api.go parseResponse). Stamp provider/model/Usagefrom the chat client. - Probe (
R-REV-10):pkg/chathas no readiness probe, so keep our own — key configured (from the keychain) and a cheap reachability check (per-provider host dial / a lightweight models call); returnsAvailability. - Keep unchanged: the
pkg/reviewengine (Critic/Review/Rubric/Availability/Usage,markdown.go, rubric catalog),shootsidecar storage, thekrites reviewcommand + off-MCP gating,criticprovider's keychain resolution + provider selection. - Config:
review.provider(gemini|claude) +review.modelmap ontochat.Config; the key stays keychain-only (§3.1). - Env-gated integration test against a real key (mirrors the GTB chat media
integration tests); unit-test the
critiqueOutput → review.Reviewmapping with a fakechat.ChatClient.
8. Non-goals (this spec)¶
- Replacing or influencing the cull. Review never sets a verdict or a rating
(
R-REV-6); it is advisory + educational. - Auto-review. It is strictly on-demand — never run across a shoot automatically (that would be slow, costly and privacy-hostile).
- Editing from the review. A review may suggest a crop or a grade; acting on it is a separate develop step the user takes deliberately.
- Storing images off-machine. Only the transient review call sends pixels; no cloud copy is retained by krites.
9. Non-destructive & determinism notes¶
Reviews are records, not edits — the original is never touched (R-ND-1). Unlike
the deterministic core, a generative review is not reproducible (the same
frame + rubric may yield different prose); that is acceptable because a review is
advisory and captured with its provider/model/timestamp, not a pipeline input. The
deterministic engine and the WASM boundary are unaffected — the Critic adapters
are provider packages, never imported by the engine.
10. Open questions¶
- Q1 — provider default & credentials. ✅ RESOLVED (2026-07-04, Matt): no default — review is OFF until Hailey supplies an API token. There is no ambient/default provider; the feature is inert until she picks a provider and provides its key through the studio. The credential-handling contract is §3.1.
- Q2 — consent granularity. ✅ RESOLVED: config opt-in + first-use confirm. Enabling review is a deliberate config act (turning it from off to on for a chosen provider); a first-use confirmation dialog discloses the egress; after that, no per-run dialog — the persistent privacy indicator + a per-run image count stand in (§3).
- Q3 — structured schema. ✅ RESOLVED: number + label + prose per dimension. Each dimension carries a numeric score, a qualitative label, and prose, plus an overall viability band (§5).
- Q4 — comparative-batch shape. ⏳ Resolve by spike (not on paper). A
quality A/B of "one combined multimodal call" vs "per-image + synthesis" on real
frames decides it during slice 5; the
Criticseam supports both, so nothing is gated. (Instinct: adaptive — single call when small, synthesis when large.) - Q5 — MCP exposure. ✅ RESOLVED: gated OFF MCP (
setup.ExcludeFromMCP, the export/remove pattern). Review egresses images, so an agent must not be able to trigger it; CLI + studio only (§6). - Q6 — local VLM candidate. Which on-device multimodal model (quantised Qwen-VL / LLaVA / MiniCPM-V-class) and runtime, for the egress-free path. Track; does not gate the cloud slices.