<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kinomig]]></title><description><![CDATA[Notes from building Kinomig, a movie and TV catalog: search, LLMs, PHP and the small decisions behind them.]]></description><link>https://kinomig.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ada82a9e08533a0d7ee4019/b3202ac8-7ecf-4072-ac9b-279e5ed60c0b.jpg</url><title>Kinomig</title><link>https://kinomig.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 07:55:19 GMT</lastBuildDate><atom:link href="https://kinomig.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The LLM Only Recognizes, It Never Invents: Building "Describe the Plot, Find the Film"]]></title><description><![CDATA[Two-pass lookup, a hard token cap, and why the model's answer is treated like user input]]></description><link>https://kinomig.hashnode.dev/the-llm-only-recognizes-it-never-invents-building-describe-the-plot-find-the-film</link><guid isPermaLink="true">https://kinomig.hashnode.dev/the-llm-only-recognizes-it-never-invents-building-describe-the-plot-find-the-film</guid><category><![CDATA[llm]]></category><category><![CDATA[PHP]]></category><category><![CDATA[search]]></category><category><![CDATA[openrouter]]></category><dc:creator><![CDATA[Kinomig]]></dc:creator><pubDate>Thu, 22 Oct 2026 21:50:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ada82a9e08533a0d7ee4019/ea0582e9-556b-4575-9aeb-8384fcdf6f6d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Two-pass lookup, a hard token cap, and why the model's answer is treated like user input</em></p>
<p>Everyone has typed something like "movie where a guy keeps waking up on the same day" into a search box. Keyword search fails at this. A language model is very good at it, and also very good at confidently naming a film that does not exist.</p>
<p>On <a href="https://kinomig.tv/">Kinomig</a>, a Russian-language movie and TV catalog with about 107,000 titles, we added a small window next to the search field: describe the plot in your own words, get the page. Here is how it is built and what broke along the way.</p>
<p>Rule one: the model recognizes, the catalog decides</p>
<p>The model is never allowed to produce the answer the viewer sees. It returns up to three candidates as JSON — title and year, ordered by confidence — and that is all. The server then looks those candidates up in our own database and renders cards for the pages it finds.</p>
<p>If the model names a film we do not have, the viewer reads "recognized, but not in the catalog". No made-up descriptions, no invented links. A wrong guess costs one useless card instead of a fake page.</p>
<p>Matching is deliberately loose in two places:</p>
<ul>
<li>Title: Russian or original, compared in full. The database collation ignores case and the Russian letter "ё", which saves a whole class of misses.</li>
<li>Year: plus or minus one. A festival premiere and a theatrical release often land in different years, and the model and our data do not always agree on which one counts.</li>
</ul>
<p>Among namesakes we prefer the exact year first, then the more popular title.</p>
<p>Two passes: memory first, web second</p>
<p>The first request goes to a cheap flash model with no tools. It costs a fraction of a cent and knows classics perfectly well.</p>
<p>It does not know new releases. In a test on September 30 we described Sinners (2025). Without web access the model answered From Dusk Till Dawn. Close in mood, wrong by thirty years.</p>
<p>So if the first pass is unsure, or names something we do not have, we repeat the request with web search enabled through OpenRouter's web plugin (five results). That costs around $0.0075 per request — cheap, but twenty times the first pass, so it only runs when memory fails. And if the web pass errors out, we show the memory answer rather than nothing.</p>
<p>The token cap is a security setting</p>
<p>Three candidates in JSON fit in roughly 150 tokens. We set a hard output limit instead of asking politely in the prompt, because the viewer's description goes straight into the prompt, and a description can talk the model into writing an essay. A limit in the request cannot be talked out of.</p>
<p>The cap bit us once. With web search on, the model first reasons about the results, and that reasoning ate the whole budget — the JSON came back empty. The lesson: when you add tools, re-measure what "enough tokens" means.</p>
<p>The model's answer is untrusted input</p>
<p>The same way we validate what the viewer types, we validate what the model returns. It may wrap JSON in a code fence even in JSON mode. It may return an array where a string is expected. Anything that does not fit the schema is thrown away, never cast into something printable. An empty title becomes NULL in the query, otherwise it would match every film that has no original title.</p>
<p>Limits that actually hold</p>
<p>Each request spends real money, so the window is for signed-in viewers only, with a small daily allowance per account. Two details mattered more than the number itself:</p>
<ul>
<li>Only successful recognitions are charged. If the model did not recognize the film, or the answer came from cache, the allowance stays. Separately, there is a ceiling on attempts (three times the allowance) and a site-wide daily cap, because nonsense input is not charged but still costs us.</li>
<li>One request at a time per viewer. The limit is checked before calling the model. Without a per-user lock, ten parallel requests would all pass the check together. A MySQL GET_LOCK keyed by user id fixed that in one line.</li>
</ul>
<p>One more small thing: the model can think for up to fifty seconds, so the endpoint closes the PHP session right after identifying the viewer. Otherwise every other tab of that viewer would wait on the session lock.</p>
<p>The most useful log line</p>
<p>Every request is logged: the description, what the model named, which pages matched, whether the web was used, what it cost. The rows where the model recognized a film and we had nothing are the best part. Each one is a free, very precise signal of what viewers came for and did not find.</p>
<p>Open question for anyone who has built something similar: how do you evaluate recognition quality without a hand-labeled set of plot descriptions? Ours is still "a dozen descriptions we wrote ourselves", and I do not trust it much.</p>
]]></content:encoded></item></channel></rss>