<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Brenner Cruvinel - av1</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/av1/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://brennercruvinel.blog"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-03T00:00:00+00:00</updated>
    <id>https://brennercruvinel.blog/en/tags/av1/atom.xml</id>
    <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>
</feed>
