Live — App Store
KinoLog
A film diary that guesses how much you will like a film before you watch it, and shows you why it thinks so.
A film diary that commits to a rating before you watch, then measures itself against what you actually thought.
- Platform
- iOS, Web
- Status
- Live — App Store
- Demonstrates
- Evaluation loops in production
- Launched
- August 2026
02
What it is for
For people who already keep a film diary and suspect it is only a list.
A recommender that never learns whether it was right is a toy. So the product is not the recommendation, it is the scorekeeping around it: the app commits to a number before you watch, settles it against what you actually typed, and prints its own hit rate whether or not that number flatters it. Below about five settled films it says it is still calibrating rather than showing a percentage.
03
Why it exists
A film diary tells you what you watched. It does not tell you anything about you that you can check.
The wager here is that a recommendation cannot be judged inside the session — its quality is only legible after you have watched the thing — but a claim about you is checkable in one second. So the app states a number out loud before you watch, and when you log the film that number is measured against what you actually said.
04
Screens





05
What it does
Rating prediction
A committed star rating before you watch, from a nearest-neighbour model over your own diary. No model call.
Prediction settlement
Logging a film turns its prediction into a settled result, scored spot-on, close, or off.
Calibration loop
Settled predictions produce a shrunk, capped bias correction that feeds back into the predictor.
Taste signals
An explained-variance measurement per axis — director, subgenre, genre, era — that also sets the model's weights.
Letterboxd import
CSV and zip ingestion with TMDB matching, conflict resolution and cancellation.
Calibration deck
An endless deck of recognisable films; every reaction feeds taste, watchlist, diary or the never-again list.
Movie night
Guest profiles with their own seen-lists, so the couch learns from the diary rather than from a chore.
MCP server
Eleven tools over stdio with a revocable personal token, so Claude can read the diary and log a watch.
06
What it runs on
Client
- 01
- React 19
- 02
- Next.js 16 App Router
- 03
- Tailwind v4
Backend
- 01
- Postgres
- 02
- Drizzle ORM
- 03
- PGlite (dev and test)
- 04
- Zod 4
- 05
- Pino
Services
- 01
- Anthropic — claude-sonnet-5
- 02
- TMDB
- 03
- Stripe
- 04
- Docker on Node 22
07
How it's built
A Next.js 16 application with 85 route handlers and a 32-table Postgres schema behind Drizzle, deployed as a single container. There is no separate backend service. The whole intelligence layer lives in about 58,000 lines under src/lib, and the interesting split is between what is computed and what is generated.
Prediction, taste-signal measurement and calibration are pure arithmetic. Recommendation prose, taste profiles and the prediction sentence go to Claude through one client with forced tool use, a JSON Schema derived from the caller's Zod schema, a circuit breaker, and per-call metering against a daily spend cap. Every LLM call is recorded with model, latency, tokens and estimated cost; unknown model ids price at the most expensive known rate, because a kill switch has to overcount before it undercounts.
The deploy gate — typecheck, lint, formatting checks — runs inside the Docker build rather than beside it, so a failing gate fails the artifact and there is no path to production that skips it.
08
Architecture
One figure, read left to right. Border treatment carries the node type; the color is the project's.
- Service
- Store
- External
- Model
09
A piece of the code
src/lib/predictRating.ts
/** Below this, silence. Five settled films cannot describe a person's tilt. */export const CALIBRATION_MIN_SETTLED = 6;/** Shrinkage half-life, in settled predictions. */const CALIBRATION_PRIOR = 8;/** The most this is ever allowed to move a prediction. */export const CALIBRATION_CAP = 0.4;export function calibrationFrom( pairs: { predicted: number; actual: number }[],): PredictionCalibration { const settled = pairs.length; if (settled < CALIBRATION_MIN_SETTLED) return { bias: 0, settled }; const raw = mean(pairs.map((p) => p.actual - p.predicted)); const shrunk = raw * (settled / (settled + CALIBRATION_PRIOR)); const bias = Math.max(-CALIBRATION_CAP, Math.min(CALIBRATION_CAP, shrunk)); return { bias: grid(bias), settled };}Excerpt — trimmed for reading, not a full file.
10
The hard parts
01 — Problem
The first predictor averaged ratings across genre, people and decade. A genre is a shelf, not a comparison: the average over it is almost exactly the person's overall average by construction, so the number looked arbitrary because it was the baseline in disguise. No film was ever compared to another film, and dislikes vanished into the averages.
Approach
It was rebuilt as a content-based nearest-neighbour model over the person's own rated diary. Similarity is a weighted sum over shared director, writers, cast, subgenre, genre, era, runtime and TF-IDF overlap of the synopses, with IDF weighting so a shared "Drama" counts for almost nothing. It takes at most eight neighbours above a similarity floor and predicts a similarity-weighted deviation from that person's own baseline.
Tradeoff
Eight neighbours rather than fifty, deliberately: at fifty the neighbour set is the shelf again with extra steps. It cannot generalise beyond films you have already rated, and it says nothing at all until you have rated eight.
02 — Problem
The prediction panel shows its working — your average, the closest films, the walk-out penalty, the final call. The one thing anyone does with a column of numbers is add it up, and three separate things make the raw contributions drift from the printed answer: a spread clamp, rounding to tenths, and the half-star floor and ceiling.
Approach
The displayed contributions are rescaled to the total that was actually applied, in proportion to their raw magnitude, with the largest absorbing the rounding remainder. Every effect keeps its sign and its rank. Anything that rounds to zero is dropped rather than printed as "+0.0".
Tradeoff
The numbers shown are a faithful rescaling rather than the raw computed ones. That is a real cost, accepted because a column that does not reconcile has disproved its own claim to be showing the real computation.
03 — Problem
A model that commits to a number in public is wrong in a direction, systematically, per person. Ignoring that leaves the only free signal on the table; over-fitting to it means chasing noise from five data points.
Approach
Settled predictions produce a mean signed error, shrunk toward zero by a factor of n/(n+8), then clamped to plus or minus four tenths of a star and rounded onto the same grid the prediction prints on. Below six settled films it says nothing.
Tradeoff
One scalar. It can correct "we run half a star low for you" and cannot correct "we run low on horror and high on comedy". Deliberately under-powered: it can never do much damage, and it can never do much good.
04 — Problem
The similarity weights were a fixed prior — a sensible ordering for a stranger, and the only weights anyone ever got. Meanwhile a separate part of the app had been measuring, for months, exactly how much each axis explained a given person's ratings.
Approach
A one-way ANOVA over the person's own diary produces an explained-variance share per axis, with groups too small to be a category dropped and axes below a noise floor omitted rather than reported as findings. That share is mapped into a bounded multiplier on the prior. An axis that was measured and rejected is nudged down rather than left alone — it was looked at and found wanting, which is itself information.
Tradeoff
Bounded on purpose, so no axis can vanish and none can dominate. Someone with genuinely extreme taste gets less benefit than the measurement would allow.
Ask about KinoLog
Answers are drawn from the notes and source behind this case study, and cite what they read.
11
What it did
6 Aug 2026
Released
1.0
Store version
15.0
Minimum iOS
Free
Price