Technical notes
Six taps, and no magic
Tonight is a content based recommender with no user data, no watch history and no collaborative signal. Everything it knows about you it learned in the last thirty seconds, in your browser, and then forgot.
- Titles
- 13.4K
- Services
- 16
- Dimensions
- 48
- Indexed
- Aug 27, 2026
01 / Offline
Building a taste space
Every title is written down as a bag of weighted terms: its keywords, its genres, and whoever directed or created it. Genres are boosted because nobody wants a comedy when they asked for horror, and makers hardest of all, because there are few of them per title and they say a great deal.
Those bags go through TF-IDF, so a tag on half the catalogue counts for almost nothing and a tag on forty titles counts for a lot, then through a truncated SVD down to 48 dimensions. That reduction is the step that does the real work: it is what puts slasher next to supernatural horror without anybody writing that rule down.
terms = keywords·1.0 + genres·2.0 + makers·2.5 X = tfidf(terms) # sparse, ~29k vocabulary U, S, _ = svds(X, k=48) # truncated SVD V = normalise(U · S) # unit rows, cosine = dot product store = int8(V) # a quarter of the bytes, same ranking
02 / Client
Learning from a tap
After the first question Tonight stops asking and starts showing, because people are unreliable at describing their own taste and very good at choosing between things.
The important part is what a tap actually means. It is not likes this. It is chose this over those three. Treating the pick as a lone positive throws away most of the signal, because the rejections are what say which direction to move.
contrast = v_picked − mean(v_shown \ v_picked) u = normalise(u + contrast) # preference, unit length pool = topK(pool, cos(u, ·), 0.38) # keep the closest 38%
Five rounds takes a couple of thousand candidates to roughly a dozen. The pool never narrows past fourteen: one option is a verdict, not a choice.
03 / Client
Choosing the four
Two constraints fight each other, and both matter. Four near identical posters teach the model nothing, so the options are picked by farthest point sampling to sit in genuinely different regions of what is left.
And people tap the poster they recognise. Left alone that turns the whole thing into a popularity chart, so each round is drawn from a single band of comparable notoriety. With fame held constant, the tap can only be about taste.
04 / Client
The shortlist, and the dial
Ranking on similarity alone returns five versions of the same film, which is the classic failure of every content based recommender. Maximal marginal relevance discounts a candidate by how much it resembles what has already been chosen, and the serendipity slider is the coefficient.
score(t) = λ·cos(u, t) − (1 − λ)·max cos(t, s) for s in chosen λ = 1 − serendipity # 1.0 pure relevance, 0.0 pure difference
A reroll is not a shuffle. It rejects five titles at once, which is the strongest single signal the model ever receives, so it pushes away from their centroid rather than paging down the same ranking.
05 / Architecture
Where the work happens
A round trip per tap would kill the feel, and the feel is the product. So the server does the constraining and the browser does the narrowing: your services, mood and time cut the catalogue to a few hundred candidates, those ship once as quantised vectors, and every tap after that is local arithmetic.
sync CLI ──▶ SQLite ──▶ FastAPI ──▶ Next ──▶ browser (only path /pool proxy narrowing that calls ~570 KB + Zod + ranking a provider) gzipped boundary (0 requests)
Page views never reach an upstream provider. However much traffic this gets, it cannot spend a single API request.
06 / Data
What gets in
The floor is on how well a title is described, not on how popular it is: a poster, an overview, at least three keywords, twenty five ratings, and for films a runtime over forty minutes.
A title with two keywords has a meaningless position in the space and would surface as a baffling suggestion. A small film that somebody bothered to tag properly stays in.
07 / Honesty
What it cannot see
- Whether you enjoyed it. There is no ground truth. Convergence is measurable; being right is not.
- Other people. No collaborative filtering, no users like you. Nothing about you is stored or sent anywhere.
- Right now. Availability is a snapshot from the last sync. Catalogues change constantly.
- Your country. Everything here is the United States catalogue. Elsewhere the same services carry different things.
08 / Colophon
Built with
- Frontend
- Next.js, React, TypeScript, Tailwind
- Backend
- FastAPI, SQLAlchemy, SQLite, Alembic
- Engine
- NumPy, SciPy sparse SVD
- Catalogue
- 9,243 films, 4,163 series