My CCTV Search Box Now Searches October
Triloch, part 6: two local models described 698 October clips in three days. Search finds them now, if you type walks instead of walking.
Triloch is the search engine I’m building for my home CCTV recordings. It runs on my own two MacBooks, and the search never looks at video. A vision model watches each eight-second clip and describes it, a second model pulls short claims out of that description, and search reads only those claims. When I finished part 5, that pipeline had worked on exactly two clips.
That works on real footage now. My activity filter from part 2 flagged 698 clips across nine days of October, and the search box looks through 2,739 claims from them, playing each result beside the words that matched. Getting there took three days of model time I’d budgeted at seven hours, a parser that was stricter than the model it was reading, and a search for “walking” that found nothing at all.
Seven hours, planned
The plan was simple. Marlin, the vision model, would describe every clip, and then Qwen, a text model, would pull claims out of every description. A trial over a dozen clips averaged about 27 seconds per description, which put the full job at roughly five hours of Marlin and two of Qwen. A coding agent ran the batch and handled restarts, and anything that changed code came to me first.
The run started on the evening of 1 November and got slower almost immediately. Over its first 44 minutes, Marlin dropped from about 48 tokens per second to 18 (a token is a small piece of text the model generates). Qwen went from about 50 to 15, and then one of its replies ran past the 120-second limit and stopped the batch.
I suspected heat, and raised Qwen’s limit to 300 seconds to get past it. That was the expensive mistake. The limit is part of each saved result’s identity, so every extraction already saved had to run again, and the slowdown carried on through two more days of stops.
Heat never really fit. A temperature monitor showed the chips at full clock speed during generation. The obvious test was to run one slow prompt on its own, and it took us three days to try it. Alone, the prompt produced 2,049 tokens in 35.7 seconds, about 57 tokens per second. Inside the batch the same reply crawled along at about 7, with the GPU drawing close to 1 watt. The GPU was sitting idle, waiting on my code.
After every token, the batch saved a progress file. Before each save, the records store walked the entire folder to check it was still under its size and file limits. With 100 files that check cost 1.8 ms. With 6,600 files it cost 55 ms, on every single token, so the more of the batch that finished, the slower each new token got. My trial had run against a nearly empty folder, which is why it never showed any of this.
The fix was to save progress at most once a second. After that, a clip took about 22 seconds for Marlin and 9 for Qwen, and a reply that had once run past 300 seconds finished in 46. The run ended on 4 November, after nine restarts, with 693 complete descriptions.
What I do differently now costs a few minutes. When a model slows down, I run the same prompt on its own before I believe any explanation for it.
Typing on the laptop keyboard, four times
Qwen turned 495 of the 693 descriptions into valid claims, and my parser rejected most of the rest. A sample of the rejections pointed at formatting slips, but it only counted the first error in each reply. Counting every error showed the biggest cause was Marlin repeating itself. Here’s part of one description, and what my parser did with it:
Honestly, a fair summary of my October. Qwen had done its job here. It quoted “The person types on the laptop keyboard.” word for word, because every claim has to point at the exact text it came from. But my parser also insisted that each quote appear exactly once, so a sentence Marlin wrote four times couldn’t be quoted at all. Across the batch, that one rule rejected 294 quotes.
So I loosened the parser only where loosening didn’t mean choosing a meaning. A repeated quote now points at its first exact match, which tells me where the words are without claiming to know which line Qwen meant. That, plus a couple of similar fixes, brought back 86 replies without running a model.
Everything else would have needed a guess, like a fractional frame number or one key holding two different quotes, so the parser still rejects those. For them I ran Qwen again with its output constrained to the exact JSON shape the parser expects, and 111 of 112 came back valid. That puts claims on 692 of the 693 descriptions.
Searching October
Search scores each claim with BM25A classic keyword-ranking formula. It scores text higher when it contains the words you searched for, gives rare words more weight than common ones, and stops rewarding a word after it has repeated a few times. It matches the exact words, so it won’t connect “walking” to “walks”. and gives each clip the score of its best claim. I choose a camera and a day and get back up to three clips, each playing next to the claim that matched, the model and prompt that produced it, and the recording it came from.
Before trying it, I wrote down eight short queries and committed them, so the results couldn’t shape the questions. “Door” found matches on eight of the nine days. “Walking” found nothing on any day, which looked like a bug until I counted the claims. “Walks” appears 92 times and “walking” never does. Other verbs go the same way: “sits” appears 542 times against 25 for “sitting”, and “stands” 165 times against a single “standing”. This keyword search matches the exact word I type, so “walking” had nothing to find.
What the box does now
For the first time, I can ask the October footage a question myself. I type a word, pick a day, and watch the three best matches with Marlin’s sentence and Qwen’s claim beside each one. When a claim is wrong, the way the door in part 5 was, I can see which model said it and which prompt it was answering. Playback and the evidence work, and the results are usable, though I haven’t measured how many of the right clips a search misses.
An empty result still doesn’t mean nothing happened, because nothing outside the clips the activity filter kept was ever described. October 2 stays held out for a test I won’t tune against.
Next comes matching by meaning, so a search for “walking” can find all 92 of those walks.
The Triloch series so far
- Part 1Building a Local-First Search Engine for CCTV Footage
- Part 2Why My CCTV Filter Lost Me When I Stopped Moving
- Part 3I Built My CCTV Test Set With AI Agents
- Part 4Qwen, Gemma and Marlin Captioned My CCTV Footage
- Part 5Getting My CCTV Captions Ready for Search
- Part 6My CCTV Search Box Now Searches October