<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Brenner Cruvinel - nest</title>
    <subtitle>AI researcher and product designer building at the edge of computer science and mental health.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://brennercruvinel.blog/en/tags/nest/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://brennercruvinel.blog"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-12T00:00:00+00:00</updated>
    <id>https://brennercruvinel.blog/en/tags/nest/atom.xml</id>
    <entry xml:lang="en">
        <title>Validating on disk the benchmark I wrote in the middle of the night</title>
        <published>2026-09-12T00:00:00+00:00</published>
        <updated>2026-09-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/validando-no-disco-o-benchmark-que-escrevi-de-madrugada/"/>
        <id>https://brennercruvinel.blog/en/blog/validando-no-disco-o-benchmark-que-escrevi-de-madrugada/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/validando-no-disco-o-benchmark-que-escrevi-de-madrugada/">&lt;p&gt;I opened a new session, with no context at all, and asked as if I were someone who had just cloned the repo: how do you use Nest, does it have an interface? The first command the answer suggested returned &lt;code&gt;command not found&lt;/code&gt; on my own machine. The second one showed a default model I was sure I had swapped out weeks earlier. Two holes no test catches, found in five minutes, and that&#39;s when I realized the topic of the week wasn&#39;t compression.&lt;/p&gt;
&lt;p&gt;The benchmark was just the place where it became visible. So, before publishing any number from the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;series&lt;/a&gt;, I spent a whole day validating: every number against the file on disk.&lt;/p&gt;
&lt;h2 id=&quot;what-was-wrong&quot;&gt;What was wrong&lt;/h2&gt;
&lt;p&gt;The first thing that didn&#39;t match wasn&#39;t even a number. I ran &lt;code&gt;strings&lt;/code&gt; on the release file and out came &quot;Spellbook MTG corpus&quot;. The file is called mtgdataset. The rename from a week earlier, which swapped Felipe&#39;s app name for the generic name in every text file, never made it into the binary. The lock next to the release described a file with the new name while the file had the old name inside, and no tool flagged it, because validation checks the file against itself. I rebuilt both releases with the right name inside, and the media bytes came out identical, which at least proved the encode is reproducible.&lt;/p&gt;
&lt;p&gt;The sizes of the three most aggressive builds were in mebibytes labeled as gigabytes. &lt;code&gt;du&lt;/code&gt; talks in MiB and I pasted it as GB. The difference is five percent per row, but the ratio inherits the error: the search build I had announced as 7.8 times smaller than the source is 7.5. The AVIF I said was 20% smaller than the recommended build is 13%. No conclusion changes, and it still made me ashamed, because it&#39;s the kind of mistake I&#39;d point out in someone else&#39;s report.&lt;/p&gt;
&lt;p&gt;The strangest one was the manifest of the AVIF build, which records a 21 GB source. The source is 4. I couldn&#39;t make sense of it until I divided by the number of cards: it gives the size of an uncompressed image, which is what the encoder receives midway through. The backend counted the intermediate instead of the JPEG. One line to fix, one test to write, and until then the ratio in that manifest was made up.&lt;/p&gt;
&lt;p&gt;And there&#39;s what can no longer be verified. I deleted the first full build when the disk filled up, so the sentence &quot;the three builds share the same hash&quot; is now two-thirds checkable. The CapCut experiment and the reprint matrix left no artifacts, only text. I told it to delete them myself, to free up space. Disk space is cheaper than trust and I should have known that.&lt;/p&gt;
&lt;h2 id=&quot;the-other-times-i-read-the-summary-and-not-the-file&quot;&gt;The other times I read the summary and not the file&lt;/h2&gt;
&lt;p&gt;The 200-card summary. I asked for the whole dataset, got a summary that talked about the corpus as if it were the corpus, and it was a pilot. I noticed with a meeting already scheduled. The agent didn&#39;t lie, it summarized the scope without saying so, and I read the summary instead of the disk. Since then every build delivery comes with validation of the real file, and this post exists for the same reason.&lt;/p&gt;
&lt;p&gt;My machine&#39;s paths. Days after the benchmark I found my home directory path in dozens of files headed for GitHub, inside manifests, locks, specs. The fix was an environment variable and a script that rewrites the sidecars before committing. And before that the agent had wanted to store my preferences in a local memory file. I asked it to delete it. If the workflow depends on what it remembers about my machine, it won&#39;t run in the next repo, and it won&#39;t run on the machine of whoever clones it.&lt;/p&gt;
&lt;p&gt;The five-gig repo. At some point I asked how ML engineers deal with this, because GitHub won&#39;t take it. The answer I adopted: not a single image byte goes into git. Each sample is a list of ids with the rule that generated it, and whoever has the SQLite database rebuilds the exact sample. The &lt;code&gt;.nest&lt;/code&gt; files go to Hugging Face with a hash alongside, and the repo keeps the lock, the clean manifest and the hash. The whole report is generated from each experiment&#39;s JSON, and a CI check fails if the markdown differs from the JSON. I can no longer paste a wrong number into a report without CI complaining, which is the kind of protection I needed against myself.&lt;/p&gt;
&lt;p&gt;The commits. The agent prefers one fat commit at the end of the day. I asked for sixteen, separate, with descriptions, because the history has to be readable for whoever comes later. And the plans: I rejected three in a row because they treated the model comparison as a footnote to a build already decided. The model list that exists in Nest today only exists because I insisted that research becomes a feature, never a footnote.&lt;/p&gt;
&lt;p&gt;What these episodes have in common is just one thing: in all of them I accepted a layer between me and the file. Summary, manifest, memory, fat commit. And the layer was wrong and didn&#39;t say so.&lt;/p&gt;
&lt;h2 id=&quot;what-i-ll-try-next-time&quot;&gt;What I&#39;ll try next time&lt;/h2&gt;
&lt;p&gt;Something that put me more at ease, in the middle of the validation, was finding out that the two findings I liked most already had names. The redundancy between images in a collection, which inter exploits, has been called set redundancy since &lt;a rel=&quot;external&quot; href=&quot;https://repository.lsu.edu/gradschool_disstheses/6258/&quot;&gt;Kosmas Karadimitriou&#39;s 1996 doctoral thesis&lt;/a&gt;, and the trick of lining images up as a pseudo-video has a decade of literature behind it. And the distinction between visual quality, vector stability and search usefulness, which I thought I had invented, is everyday business in the field the standards call coding for machines: it evaluates with task metrics, like mAP, precisely because &lt;a rel=&quot;external&quot; href=&quot;https://arxiv.org/abs/2110.09241&quot;&gt;feature fidelity and task performance can drift apart&lt;/a&gt;. I didn&#39;t discover anything. I confirmed two things on a corpus nobody had looked at, and that makes me calmer about publishing than if it were something new.&lt;/p&gt;
&lt;p&gt;The 1.3 GB file is already in the pasta.cards repo, waiting for the screen that will use it. What&#39;s left of the benchmark fits in a paragraph: measure search with WeMM, build the full reprint corpus, decide whether AVIF becomes the default. None of the three has a date.&lt;/p&gt;
&lt;p&gt;What I&#39;ll try is the cheapest thing of all: open a clean session as a user before every release and type the first command in the readme. Five minutes, and it found two holes a week of agents didn&#39;t. I don&#39;t know if I&#39;ll remember to do it. But now it&#39;s written down, and I trust what&#39;s written more than what I remember.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Raising the CRF until search breaks</title>
        <published>2026-09-05T00:00:00+00:00</published>
        <updated>2026-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/subindo-o-crf-ate-a-busca-bugar/"/>
        <id>https://brennercruvinel.blog/en/blog/subindo-o-crf-ate-a-busca-bugar/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/subindo-o-crf-ate-a-busca-bugar/">&lt;p&gt;The question from the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/no-final-do-dia-ta-a-mesma-coisa-1-3-gb/&quot;&gt;previous night&lt;/a&gt; was simple to ask and I took my time asking it. The app keeps every card&#39;s JPEG cached for display. The &lt;code&gt;.nest&lt;/code&gt; doesn&#39;t need to be pretty, it needs to answer which card it is. So the CRF, the codec&#39;s quality setting, can go up until the vector breaks, and the vector breaks well after the eye does. If that were true, it would even be worth thinking about two tiers: a thumbnail inside Nest for search, and the full card on demand when the app needs to show it.&lt;/p&gt;
&lt;p&gt;I had a way to measure this, or thought I did. Nest&#39;s quality gate has two sides: a visual metric, which compares the decoded frame with the source, and vector drift, which measures how much the CLIP vector moves between the original image and the decoded one. The idea behind drift is that an image can look fine and still have moved in search space. I trusted that metric. It was mine, I had insisted on it in the plan, and for good reason: on dermoscopy it had caught a case where the image passed by eye and search by clinical text dropped by half.&lt;/p&gt;
&lt;h2 id=&quot;the-gate-rejected-the-whole-ladder&quot;&gt;The gate rejected the whole ladder&lt;/h2&gt;
&lt;p&gt;I took the visual side out of the gate and left only drift, with the floor Nest has used since dermoscopy. I had the CRF climb a ladder from 40 to 60. The gate rejected the whole ladder, including the first rung, and fell back to the most conservative one because it had nowhere else to go. Drift was already below the floor at CRF 40 and only got worse. And the visual metric, which I had taken out of the gate but kept computing, told the same story from another angle: from CRF fifty on it gave negative values on some cards, something I didn&#39;t even know it could do.&lt;/p&gt;
&lt;p&gt;The file the gate delivered, at CRF 40, the lowest on the ladder, came out close to one gig, four times smaller than the source. It was already smaller than the recommended build, and the gate had rejected it too, it just had nowhere lower to fall.&lt;/p&gt;
&lt;p&gt;By the gate&#39;s logic, none of these files was usable. I built one at CRF fifty anyway, no gate, just to see the size. Half a gig. Seven and a half times smaller than the source, and the cards turned into a blur, rules text unreadable, the art recognizable only from a distance. A file I wouldn&#39;t show anyone.&lt;/p&gt;
&lt;p&gt;Then I ran the metric that actually matters, which I hadn&#39;t run before because I was too lazy to set it up: search by name. A hundred queries like &quot;the art of such-and-such card&quot;, on the five files I had of the full corpus, from the 3.6 GB lossless one down to the half-gig blur. The first round gave zero hits on two of the five files, the lossless and the recommended one, and for a minute I thought I had broken search. It was the bench: it matched results by the card&#39;s position in the stream, and the two files with similarity sorting have a different order. Nest has a content id per item for exactly this, and once the bench matched on that, all five files answered again.&lt;/p&gt;
&lt;p&gt;And accuracy stayed within the noise on all of them. The half-gig file finds the right card as well as the 1.3 GB one, and both stay within the noise of the lossless file.&lt;/p&gt;
&lt;h2 id=&quot;search-survives-far-more-compression-than-the-eye&quot;&gt;Search survives far more compression than the eye&lt;/h2&gt;
&lt;p&gt;I stared at that on screen for a while. Cosine drift measures whether the vector moved. Search measures whether the vector still finds. I was using the first as if it were the second, and it would have vetoed a file that works. The vector does move, and it still stays closer to the right card than to any other. How far it travels doesn&#39;t matter if its neighbors don&#39;t change. A gate that&#39;s any good for this use needs hit@k as its floor, the fraction of queries where the right card shows up in the top k results, not cosine.&lt;/p&gt;
&lt;p&gt;That became a profile in Nest, &lt;code&gt;retrieval-only&lt;/code&gt;, which writes into the manifest that the file serves search and never display. And AVIF, which in the benchmark had come out about 12% smaller than AV1 at the same quality, held up on the full corpus: 13% smaller than the recommended build. It&#39;s the candidate to become the default backend for stills, and I haven&#39;t decided yet, because AVIF takes four to ten times longer to build and embed.&lt;/p&gt;
&lt;p&gt;There&#39;s a third metric I ran alongside that&#39;s worth nothing, and it&#39;s worth saying why. Identity: give the image itself as the query and see if it finds itself. It gave a hundred percent on every file, even the blur. Looks great and says nothing, because the query is the very image that produced the frame, and the uncompressed control also gives a hundred. It only proves the codec didn&#39;t destroy the data. I left it in the bench so nobody confuses pipeline stability with usefulness, which is exactly the confusion I made with drift.&lt;/p&gt;
&lt;p&gt;The metric is weak and I know it. It&#39;s a hundred queries with a model that does poorly on this corpus, CLIP, which only looks at the art. &quot;Didn&#39;t drop&quot; means &quot;I couldn&#39;t measure a drop&quot;, not &quot;it&#39;s the same&quot;. And WeMM, which reads the name printed on the card and gets nine in ten right, might suffer far more from the blur than CLIP, because what it reads is exactly what the CRF destroys first. I didn&#39;t measure it. That&#39;s next, along with the full reprint corpus, which is where inter has something to predict.&lt;/p&gt;
&lt;p&gt;What stuck with me that week was less about compression and more about what I choose to measure. I had a metric I designed myself, one that looked rigorous, and it answered a question I wasn&#39;t asking. And before publishing any of these numbers, I wanted to validate each one against the disk, because a report written in the middle of the night gets things wrong.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>At the end of the day it&#39;s the same thing, 1.3 GB</title>
        <published>2026-09-03T00:00:00+00:00</published>
        <updated>2026-09-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/no-final-do-dia-ta-a-mesma-coisa-1-3-gb/"/>
        <id>https://brennercruvinel.blog/en/blog/no-final-do-dia-ta-a-mesma-coisa-1-3-gb/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/no-final-do-dia-ta-a-mesma-coisa-1-3-gb/">&lt;p&gt;I was sure. Sorting the cards by similarity before encoding would multiply the compression, because similar neighboring frames are exactly what video codecs love. I had written it in the bets, I had repeated it to the agent, and the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/&quot;&gt;benchmark&lt;/a&gt; had handed me back a strange row: the variant with inter came out bigger than the one without. The configuration was wrong, scene cut detection was on, and different card after different card is one scene cut per frame. The encoder was burning cycles looking for a scene that didn&#39;t exist. I turned it off and put together a second corpus, almost three thousand real reprints of the same art, grouped together.&lt;/p&gt;
&lt;p&gt;That did it. With prediction in groups of sixteen frames, the file came out 29% smaller than without prediction. Groups of sixteen beat a single keyframe for the whole stream, which surprised me, and they still keep any card reachable by decoding at most sixteen frames. And a detail I didn&#39;t expect: shuffling the reprints instead of grouping them cost only two points. Part of the gain comes from the frame every card shares, not from the repeated art. The codec was exploiting the chrome, as I had suspected on day one, and that held for any order.&lt;/p&gt;
&lt;h2 id=&quot;building-the-whole-corpus-at-night&quot;&gt;Building the whole corpus at night&lt;/h2&gt;
&lt;p&gt;With 29% in hand, I built the full corpus with similarity sorting and prediction on, and next to it the same corpus with none of that, to compare. And I built it with the media inside the file, because by then I no longer accepted MP4s sitting next to the database. Before getting there I opened the folder of an intermediate build and found &lt;code&gt;.build&lt;/code&gt;, &lt;code&gt;.cache&lt;/code&gt;, &lt;code&gt;.forge-state&lt;/code&gt;, &lt;code&gt;.tmp&lt;/code&gt;, a lock, a manifest, all next to a &lt;code&gt;.nest&lt;/code&gt; of a few megabytes. What is all this for? I asked, and the honest answer was that every piece had a reason and none of them needed to be there once the build finished. Media inside the file was half the answer, the other half was cleaning up the output.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;.nest&lt;/code&gt; is a container of sections, each with its own hash. The new section is the simplest of all: a table of offsets and the raw bytes of the videos, glued one after another. An MP4 stays an MP4, nothing is re-encoded to fit. Opening the file doesn&#39;t touch the media, the system only maps the slice when someone asks for a card, and you can export the original MP4 back out with the hash checked before writing.&lt;/p&gt;
&lt;p&gt;That section doesn&#39;t count toward Nest&#39;s content hash, and this matters more than it seems. The content hash is computed over what&#39;s inside the file, not over the packaging, and it&#39;s what makes a Nest citation point to a piece of knowledge instead of a place on disk. The same corpus built with the media inside and with the media outside gives the same hash. It cost three percent: the indexes, the card text and the vectors add up to about forty megabytes on top of 1.3 GB of video.&lt;/p&gt;
&lt;p&gt;The builds ran overnight, and the night was rough. Two died with a full disk, because macOS was taking system snapshots in waves, eating and releasing gigabytes without warning, and at one point I was down to a hundred and thirty megabytes free. And in the middle of that I read a summary from the agent saying the corpus had 200 cards. I had a client meeting in a few hours.&lt;/p&gt;
&lt;p&gt;&quot;Seriously, you used 200 demo cards? I have a client meeting and I still don&#39;t have the dataset.&quot; I had it run validation on the 1.3 GB file. All 38 thousand blobs checked out, one by one. The corpus was complete, the summary was describing the pilot, and I had read the summary instead of the disk. After the encoder that wasn&#39;t using the tune the manifest claimed, it was the second time that week that what I read wasn&#39;t what was actually there.&lt;/p&gt;
&lt;h2 id=&quot;at-the-end-of-the-day&quot;&gt;At the end of the day&lt;/h2&gt;
&lt;p&gt;The sorted build sat next to the plain build. 1.3 GB both, no difference at all. I sent it to the agent at four in the morning: at the end of the day it&#39;s the same thing, right, 1.3 GB.&lt;/p&gt;
&lt;p&gt;The reason was in the manifest, correct this time. Nest decides per segment whether to use prediction, encoding a sample both ways. On the real corpus, made of unique cards, the probe vetoed prediction in every segment. There&#39;s nothing to predict: the next card is another card, with other art, and the shared chrome doesn&#39;t pay for the cost. The 29% gain only exists on the reprint corpus, and I haven&#39;t built that one in full yet.&lt;/p&gt;
&lt;p&gt;There was a worse part, which I only understood later. In an earlier version of the probe, it decided on bytes alone, and in a build of the sample it picked prediction for every segment because it tied on size. Median quality dropped sixteen points. The encoder compresses predicted frames with coarser quantization, and with unique cards a predicted frame is just a worse keyframe in disguise. Equal bytes aren&#39;t equal quality. The probe now measures both sides, accepts losing at most two points, and records the reason when it vetoes. The 29% matrix also compares bytes only, so the number is real but it isn&#39;t equivalent quality. I&#39;m putting that down here because I forgot it myself for a whole day.&lt;/p&gt;
&lt;h2 id=&quot;what-if-the-file-doesn-t-need-to-be-pretty&quot;&gt;What if the file doesn&#39;t need to be pretty&lt;/h2&gt;
&lt;p&gt;What I take from that night isn&#39;t the 1.3 GB. It&#39;s that my intuition about codecs was wrong twice, and that I read summaries when I should read files. Now I only turn sorting on when the corpus has near-duplicates, and no build goes in without validation of the real file running alongside it.&lt;/p&gt;
&lt;p&gt;And one question was left over that seemed more promising than any sorting. Nest&#39;s quality gate locks the codec at a conservative level because the visual metrics never pass on small cards with tiny print. But Felipe&#39;s app keeps every card&#39;s JPEG cached for display. The &lt;code&gt;.nest&lt;/code&gt; only needs to answer which card it is. If the file doesn&#39;t need to be pretty, how far can you push the setting before search starts complaining?&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The encoder had ignored tune=still since the first build</title>
        <published>2026-09-02T00:00:00+00:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/"/>
        <id>https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/encoder-ignorou-o-tune-still-desde-o-primeiro-build/">&lt;p&gt;The symptom was a number that didn&#39;t add up. I had a battery of encoders running in parallel: nineteen configurations across AV1, AVIF, x264, x265, WebP and lossy JPEG XL. Even VVC got in, more out of curiosity than hope. Each one calibrated to the same visual quality, to find out whether Nest&#39;s AV1 was the most compact option or just the one closest to home. One of the agents reproduced the Nest build by hand, with the same parameters, and the file came out a different size. Same card, same CRF, same preset, different bytes. I read that in the morning and my first reaction was that the agent had gotten the command wrong.&lt;/p&gt;
&lt;h2 id=&quot;the-probe-failed-and-nobody-said-a-word&quot;&gt;The probe failed and nobody said a word&lt;/h2&gt;
&lt;p&gt;It hadn&#39;t. The &lt;code&gt;forge&lt;/code&gt; asks the encoder for &lt;code&gt;tune=still&lt;/code&gt;, the still-picture mode, which is the right one for a stream where every frame is an independent photo. Before asking, it runs a probe to check whether the encoder accepts that mode. The probe ran without a parameter the mode requires, failed, and the code fell back to a generic mode without telling anyone. The manifest recorded the spec, which said &quot;still&quot;, not what the encoder had actually done. Since the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;first build&lt;/a&gt; I had been reading a manifest that didn&#39;t match the binary, and the only place that recorded the mode was the wrong place.&lt;/p&gt;
&lt;p&gt;The difference measured eleven points of visual quality for ten percent more bytes, at the same CRF. The old build was the worst point on the curve: smaller, but ugly in a way that isn&#39;t worth it. And nobody would have caught it by watching the video, because what a frame looks like in the player doesn&#39;t tell you which tune was used. Only bytes against the bytes of an independent encode.&lt;/p&gt;
&lt;p&gt;And it was by chance. The encoder battery existed to answer a different question, and the manual baseline encode was just its reference point. If I had calibrated the battery against the manifest instead of against an encode done from scratch, the bug would have stayed invisible, and every number in the series would have come out ten percent optimistic on bytes and eleven points pessimistic on quality. Now the manifest records the mode the encoder actually used, and the probe passes the right parameter.&lt;/p&gt;
&lt;p&gt;Even after the fix there was a second layer. SVT-AV1 has &lt;a rel=&quot;external&quot; href=&quot;https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Parameters.md&quot;&gt;two modes&lt;/a&gt; you could call &quot;still&quot;: &lt;code&gt;tune=2&lt;/code&gt;, optimized for a structural similarity metric, which is what the fixed probe started picking, and &lt;code&gt;tune=3&lt;/code&gt;, called image quality, which the documentation itself marks as still images only. The second one gives one more point of quality for one percent more bytes. Not much, but it&#39;s the right mode for still pictures, and the recommended build of the full corpus went out with it. The manifest now says which of the two, by number, because &quot;still&quot; on its own already fooled me once.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Variant on the same 2,048 cards&lt;/th&gt;&lt;th&gt;Size&lt;/th&gt;&lt;th&gt;Visual quality&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JPEG XL repack, lossless&lt;/td&gt;&lt;td&gt;1.12x smaller&lt;/td&gt;&lt;td&gt;same as source&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 from the first build&lt;/td&gt;&lt;td&gt;3.3x smaller&lt;/td&gt;&lt;td&gt;52&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 with tune=still&lt;/td&gt;&lt;td&gt;3.0x smaller&lt;/td&gt;&lt;td&gt;63&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AV1 with inter&lt;/td&gt;&lt;td&gt;2.5x smaller&lt;/td&gt;&lt;td&gt;62&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;AVIF&lt;/td&gt;&lt;td&gt;5.5x smaller&lt;/td&gt;&lt;td&gt;43&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The AVIF row looks like the winner and it isn&#39;t. It sits at a much lower point on the quality curve, so you can&#39;t compare it on the same number. Calibrated to the same quality as AV1, it comes out about 12% smaller, and that left an open question about which backend to use for stills. And inter, my favorite bet, came out bigger than &lt;code&gt;tune=still&lt;/code&gt;. I explain that in the next post, because the configuration was wrong and the problem was something else.&lt;/p&gt;
&lt;p&gt;I won&#39;t table the rest of the nineteen, but three findings stayed with me. x264, the encoder for a 2003 standard, was the fastest inside the quality window by an enormous margin, and VVC, the newest codec on the list, cost a hundred times x264&#39;s time to land in the middle of the table.&lt;/p&gt;
&lt;p&gt;Two different SVT-AV1 presets spat out byte-for-byte identical streams, because the version I use maps both to the same internal mode when the stream is all keyframes. One more case of a parameter I was passing and the encoder was ignoring, this time documented. And lossy JPEG XL, which I expected to compete, only competes at high quality, well above the window that matters for a deck tile. It&#39;s a good codec for a target this corpus doesn&#39;t ask for.&lt;/p&gt;
&lt;h2 id=&quot;capcut-was-throwing-cards-away&quot;&gt;CapCut was throwing cards away&lt;/h2&gt;
&lt;p&gt;I had written in the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/&quot;&gt;bets&lt;/a&gt; that speeding up the video cut bytes, because I had done it in CapCut and the file came out half the size. I reproduced it programmatically: speed it up sixty times, export at thirty frames per second in H.264, the way the editor does. The file came out with half the bytes. And half the cards.&lt;/p&gt;
&lt;p&gt;The export samples the sped-up timeline and drops whatever falls between one frame and the next. Of the 2,048 cards, 993 disappeared. What was left got re-encoded to H.264 on top of AV1 that was already lossy. CapCut didn&#39;t compress anything, it threw half the dataset away and called it optimization, and I looked at the file size and believed it. Changing only the fps, without speeding up, doesn&#39;t change a single byte. It&#39;s a timestamp annotation in the container, as &lt;code&gt;ffprobe&lt;/code&gt; had already told me on day one and I hadn&#39;t properly written down.&lt;/p&gt;
&lt;p&gt;The wrong bet yielded a real bug. With an fps other than one in the manifest, the code that fetches a frame by index asked for an instant that didn&#39;t exist, because it assumed ordinal and timestamp were the same thing. Fixed, and tested with the fps I never should have changed. The real gain CapCut was pointing at, redundancy between neighboring frames, does exist. It just has to be measured with inter, without losing cards, and that&#39;s the subject of the next late night.&lt;/p&gt;
&lt;h2 id=&quot;what-else-is-the-manifest-getting-wrong&quot;&gt;What else is the manifest getting wrong&lt;/h2&gt;
&lt;p&gt;I closed the day with two things validated by hash and a bigger doubt than when I started. The encoder mode was a constant I thought was on, and it had been off since the beginning. CapCut was a number I thought was compression, and it was data loss. Both lived in a manifest or an export I had read and accepted without checking against anything.&lt;/p&gt;
&lt;p&gt;If the encoder wasn&#39;t doing what the manifest said, the question left over was what else I was reading as a summary instead of validating on disk. I didn&#39;t know yet that the answer would come in the middle of the night, with a meeting on the calendar.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>My bets before compressing the cards</title>
        <published>2026-09-01T00:00:00+00:00</published>
        <updated>2026-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/"/>
        <id>https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/apostas-antes-de-comprimir-as-cartas/">&lt;p&gt;The compression can&#39;t lose anything, even if we have to invent it. I wrote that sentence in the plan, that confidently, after the &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/&quot;&gt;first build&lt;/a&gt;. Two lines later I was saying lossless video with the cards sorted by similarity was a path, and further up I had dismissed &lt;code&gt;jpegtran&lt;/code&gt; because &quot;lossless doesn&#39;t count&quot;. I only noticed when I sat down to write the bets and had to define what I was going to measure, and measuring requires saying against what. Lossless relative to what? I had no answer. I had used the same word for three things and hadn&#39;t noticed.&lt;/p&gt;
&lt;h2 id=&quot;why-zstd-can-t-bite-into-a-jpeg&quot;&gt;Why zstd can&#39;t bite into a JPEG&lt;/h2&gt;
&lt;p&gt;A JPEG has already gone through three stages that exhaust the redundancy a general-purpose compressor knows how to find. The image becomes frequency coefficients, the high-frequency coefficients get rounded (that&#39;s where the loss happens), and the result goes through an entropy coder. What comes out is a bitstream where the repeated patterns have already been eaten. zstd looks for exactly that, repeated patterns. It&#39;s like trying to fold a sheet of paper that&#39;s already folded to the limit: it doesn&#39;t get smaller, it just crumples.&lt;/p&gt;
&lt;p&gt;So &quot;lossless&quot; on top of JPEG has three meanings, and only one deserves the name. The first is repacking the JPEG in a way where the inverse operation gives back the original file byte for byte. JPEG XL does this: it takes the already-rounded coefficients and rewrites them with a better entropy coder, keeping enough to undo it. The JPEG&#39;s loss is still there, but the packaging is tighter.&lt;/p&gt;
&lt;p&gt;The second is re-saving the same JPEG with &lt;code&gt;jpegtran&lt;/code&gt; or &lt;code&gt;jpegoptim&lt;/code&gt;. The pixels stay identical and the bytes change. Lossless for whoever looks, not for whoever checks the hash. And if the source is already optimized, like the files from Scryfall, the gain tends to zero.&lt;/p&gt;
&lt;p&gt;The third is what I was proposing without knowing it: decode the pixels and recompress them with another lossless codec, PNG, WebP, or FFV1 video. It looks like the purest meaning and it&#39;s the worst. The new codec receives pixels carrying the JPEG&#39;s rounding noise as if it were signal, and spends bits representing that noise faithfully. Everything points to the result being bigger than the source JPEG, not smaller. I wrote that and still didn&#39;t believe it, hence the bet.&lt;/p&gt;
&lt;p&gt;There&#39;s a fourth thing, which isn&#39;t a meaning but a gap. In 2016, a group from Tianjin University and Microsoft Research Asia published &lt;a rel=&quot;external&quot; href=&quot;https://doi.org/10.1109/TIP.2016.2551366&quot;&gt;&quot;Lossless Compression of JPEG Coded Photo Collections&quot;&lt;/a&gt;: they arrange a collection of JPEG photos as a pseudo-video, compensate the difference between neighboring photos, remove the remaining redundancy in the frequency domain, and still give back every original JPEG without losing a bit. Average savings above 31%. That&#39;s exactly what my invented idea wanted to be, done the right way. Except I couldn&#39;t find an open implementation, and I wasn&#39;t going to write a codec that week. So what I could measure was the ceiling with the tools that exist. The real ceiling, if that paper reproduces on Magic cards, is higher, and I want to say that before giving my number.&lt;/p&gt;
&lt;h2 id=&quot;what-i-bet-and-why&quot;&gt;What I bet and why&lt;/h2&gt;
&lt;p&gt;I wrote it down before running anything, because the last time I published a number without a written bet I had to correct it two days later, and correcting a published number is worse than losing a bet in public.&lt;/p&gt;
&lt;p&gt;Does lossy AV1 video compress much better than loose JPEGs? Yes, that&#39;s what the first build showed and what I had seen with dermoscopy. The more alike neighboring cards are, the more the video gains? Yes, and strongly: the database has 100 thousand printings for 52 thousand illustrations, almost half the cards repeat art that already exists on another, and they all share the same frame. If there&#39;s a video gain, it lives there.&lt;/p&gt;
&lt;p&gt;Does speeding up the video cut bytes? I had dropped one of the MP4s into CapCut, sped it up sixty times, and the file came out half the size. I bet yes, and if it&#39;s true it&#39;s the cheapest item on the list. Does tar plus zstd gain anything? I think not, for the reason in the section above. And does sorting by similarity help with unique cards too? I think not, I&#39;ve seen sorting do nothing on another dataset, but the shared frame leaves me unsure.&lt;/p&gt;
&lt;p&gt;And the one I wanted most: there&#39;s a lossless option better than JPEG, &quot;even if we invent it&quot;. After writing out the three meanings, I changed my mind halfway through the bet. The candidate became JPEG XL, and the lossless video sorted by similarity, the invented idea, is going to lose. The two I most wanted to be true, CapCut and lossless, were the two I could least defend. That&#39;s usually a sign.&lt;/p&gt;
&lt;p&gt;The lossless run went that same night, ten variants on the same 2,048 cards. Tar plus zstd came out one to one, exactly. Re-saving with &lt;code&gt;jpegtran&lt;/code&gt; gave less than one percent, the Scryfall JPEGs are already optimized. The ceiling was the JPEG XL repack, about 11% smaller, with all 2,048 original files rebuilt and validated by hash. Pushing the encoder effort to the maximum gives less than one percent more for six times the time, so the default stayed at medium effort.&lt;/p&gt;
&lt;p&gt;And the invented idea lost badly. FFV1 and x264 without quantization, with the cards sorted by similarity, came out three to four times bigger than the source JPEG. Sorting, in that regime, barely moved the size at all, a fraction of a megabyte on a file of hundreds. A decoded JPEG pixel carries noise and no codec predicts noise, just as I had written that morning without believing it.&lt;/p&gt;
&lt;p&gt;That became Nest&#39;s archive profile: anyone who won&#39;t accept losing anything gets the JPEG XL repack, 3.6 GB for the whole corpus, and every JPEG comes back bit for bit on request. It&#39;s not the number I wanted. It&#39;s the number I can defend with a hash, and I prefer that one. The other bets, the lossy ones, were running while I wrote this. The CapCut one, I was already afraid to look at.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nest has the potential to be a Magic card archiver</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            Brenner Cruvinel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/"/>
        <id>https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/</id>
        
        <content type="html" xml:base="https://brennercruvinel.blog/en/blog/nest-archiver-de-cartas-de-magic/">&lt;p&gt;Felipe Navarro, a friend of mine, is building &lt;a rel=&quot;external&quot; href=&quot;https://github.com/ofelipenavarro/pasta.cards&quot;&gt;pasta.cards&lt;/a&gt;, an open source app for managing your collection and Commander decks. It runs locally, keeps everything in SQLite, and to work offline it downloads the art of every Magic card ever printed, in two versions: the full card and just the illustration. On my Mac that came to 83 thousand JPGs and 7 GB. Every card that exists, twice, on my disk, and Finder dragging its feet to open the folder.&lt;/p&gt;
&lt;p&gt;What caught my attention in that number wasn&#39;t the size, disk is cheap. It was the structure. Cards share the frame, the text box, the typography, the mana symbols. And the database next to it shows that almost half of all printings reuse art that already appeared on another card. A loose JPG knows nothing about that. A good catalog points instead of copying, and a good codec doesn&#39;t spend bits on a pixel it can predict from its neighbor. I like both for the same reason.&lt;/p&gt;
&lt;p&gt;I had &lt;a href=&quot;https://brennercruvinel.blog/en/blog/nest-vector-db-soberano/&quot;&gt;Nest&lt;/a&gt; at hand, the single-file vector database I&#39;ve been building this year, and since August it stores images too. The question I asked that morning was far more naive than everything that came after: how does it work, does it turn this into a video? I have 38 thousand images and a database, what does it do with them?&lt;/p&gt;
&lt;h2 id=&quot;does-it-turn-this-into-a-video&quot;&gt;Does it turn this into a video?&lt;/h2&gt;
&lt;p&gt;It really does turn it into a video. Each image becomes a frame of an AV1 stream inside the file, and search runs on vectors taken from the decoded frames, not from the source JPEG. That&#39;s on purpose: the index has to describe what the file serves, and what the file serves is the frame after the codec. Video codecs are the most optimized image compression technology there is, so what looks like a hack is just borrowing decades of other people&#39;s work.&lt;/p&gt;
&lt;p&gt;I had already measured this on skin. Dermoscopy, biopsy slides, pages of old books, the datasets I had on hand from my work at Hoff. What those numbers taught me is that how much something compresses is a property of the domain, not the codec. Same encoder, same settings: dermoscopy compressed 151 times and the book page compressed 4. Smooth skin compresses well because there&#39;s little detail to keep. Small print is the opposite, every edge costs bits.&lt;/p&gt;
&lt;p&gt;A Magic card has small print. It has the same frame on every card, which the codec may or may not exploit. It has reprints of the same art, something dermoscopy doesn&#39;t have. And it has a product constraint that no search metric measures: the deck tile has to look good on the app&#39;s screen. I thought the question was how much it compresses. It was, but not for long.&lt;/p&gt;
&lt;p&gt;Before running anything, I took a calmer look at the raw folder, and it had surprises of its own. Half the weight was the cropped version of the art, and my first idea was to throw it away and crop from the full card when needed. An agent read the app&#39;s code and came back with bad news: the database doesn&#39;t store any crop coordinates, and the app uses the crop directly in the deck tiles. The cropped art is a class of its own, not a derivative. And next to the image folder there was a directory called &lt;code&gt;_redundant&lt;/code&gt;, holding another seven gigs of copies of things that were already there. That one I deleted without mercy.&lt;/p&gt;
&lt;h2 id=&quot;the-first-build&quot;&gt;The first build&lt;/h2&gt;
&lt;p&gt;The pilot with a few hundred cards died with a segfault before doing anything. Nest&#39;s Python binding had been compiled without the flag that says &quot;use the process&#39;s Python, don&#39;t bring your own&quot;, and the Python I use is static, so two interpreters were fighting over the same memory. One flag and done. A small bug, and the kind that makes me suspicious: how many other environment assumptions does the build carry without declaring them?&lt;/p&gt;
&lt;p&gt;With the flag, the full build ran in the background and I opened one of the videos in the player. Cards going by, one per second, forty minutes of cards. My first hypothesis was that the encoder was repeating frames to hold each card on screen, and that this could be cut. One line of &lt;code&gt;ffprobe&lt;/code&gt; killed the hypothesis: one frame per card, and the duration is just a timestamp annotation in the container, it costs no bytes.&lt;/p&gt;
&lt;p&gt;7 GB became 1.9 GB, with image search working on top. It ran on the first try, which in a project of mine is rare enough to make me suspicious. Then I opened the output folder.&lt;/p&gt;
&lt;h2 id=&quot;an-archiver-with-38-mp4s-next-to-it-isn-t-an-archiver&quot;&gt;An archiver with 38 MP4s next to it isn&#39;t an archiver&lt;/h2&gt;
&lt;p&gt;38 video files, a &lt;code&gt;.nest&lt;/code&gt; with the indexes, a JSON manifest and a cache directory. Four kinds of file to serve one thing. Nest exists to be the database in one file, and what came out was a database with forty things next to it. It worked, the way a piece of furniture assembled with leftover screws works. The point of Nest was getting lost there, and compression wasn&#39;t even the problem. Nest has the potential to be an archiver for Magic cards, for maps, for any image dataset with a database beside it. But an archiver is a file, not a folder.&lt;/p&gt;
&lt;p&gt;The second question came from the model. The script used CLIP because that was what the &lt;code&gt;forge&lt;/code&gt; had, and I didn&#39;t really know what CLIP did with a card. I looked into it: it looks at the art and ignores the printed text. For a Magic card that means looking at the less useful half, because what identifies the card is the name written at the top. I asked for a search on Hugging Face and GitHub of the open multimodal models available that month, and the answer was a Tencent family called &lt;a rel=&quot;external&quot; href=&quot;https://huggingface.co/tencent/WeMM-Embedding-2B&quot;&gt;WeMM&lt;/a&gt;, which reads text inside images, plus SigLIP and Jina. I already had the two-billion-parameter WeMM downloaded for another reason.&lt;/p&gt;
&lt;p&gt;In a comparison with 1,500 cards and sixty queries like &quot;the art of such-and-such card&quot;, CLIP found the right card one time in four. WeMM got nine in ten, SigLIP came close, Jina landed in the middle. WeMM&#39;s price is being about thirty times slower than CLIP, and running it over the whole corpus would take almost a day on my machine. So the comparison stayed on the sample, and the full corpus kept CLIP for now.&lt;/p&gt;
&lt;p&gt;What came out of this wasn&#39;t &quot;switch to WeMM&quot;. It was that the model can&#39;t be a constant at the top of a script. It has to be a list of presets inside the engine, and whoever builds picks which one, or which ones, at build time. And the video has to fit inside the file, not next to it. Before either of those two things, I wanted to know how far I could compress without losing anything. Even if we had to invent it.&lt;/p&gt;
</content>
        
    </entry>
</feed>
