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

KinoLog's Tonight screen showing five film cards with predicted star ratings and the reasoning behind one of them.
01the daily set, each card carrying a number the app has already committed to
KinoLog's taste page summarising what the model has read and inferred.
02what it has read, and what it refuses to claim until film fifteen
KinoLog's diary listing logged films with their vibe tags and ratings.
03twelve films, thirteen watches — the rewatch counts twice on purpose
KinoLog's stats page showing films watched and average rating.
04plain numbers; no chart earns its place until there is something to plot
KinoLog's calibration deck for rating recognisable films quickly.
05the deck, where a cold start stops being cold

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.

deterministic pathllm pathsettled outcomeobserved errorcorrectionLetterboxd exportCSV or zipimport/runner.tsParse, match TMDB, dedupebuildPredictor()Nearest neighbour, K = 8Predicted ratingArithmetic, no model calllogsThe rating you gaveprediction_proseclaude-sonnet-5The sentenceNever the numberSettlementPredicted against actualcalibrationFrom()Shrunk bias, capped 0.4
Fig. 1prediction and calibration flow
  • 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 };}
The whole feedback loop is fourteen lines and three constants. A system that states a number in public has to be measurably wrong before it may correct itself, and even then only a little: shrinkage toward zero means six settled films move the prediction almost not at all, and the cap means forty move it by at most four tenths of a star.

Excerpt — trimmed for reading, not a full file.

10

The hard parts

01Problem

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.

02Problem

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.

03Problem

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.

04Problem

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