<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: casanovalabs</title>
    <description>The latest articles on DEV Community by casanovalabs (@casanovalabs).</description>
    <link>https://dev.to/casanovalabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057988%2F76fbdaa8-f637-448e-87ec-f031ff65476d.png</url>
      <title>DEV Community: casanovalabs</title>
      <link>https://dev.to/casanovalabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/casanovalabs"/>
    <language>en</language>
    <item>
      <title>Per-Property vs Per-Credit: The Unit-Economics Choice Behind a Media SaaS Pricing Page</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sun, 16 Aug 2026 22:08:45 +0000</pubDate>
      <link>https://dev.to/casanovalabs/per-property-vs-per-credit-the-unit-economics-choice-behind-a-media-saas-pricing-page-41gn</link>
      <guid>https://dev.to/casanovalabs/per-property-vs-per-credit-the-unit-economics-choice-behind-a-media-saas-pricing-page-41gn</guid>
      <description>&lt;p&gt;There are two ways to price an AI media tool, and the choice quietly decides what your customer optimizes for. You can bill per operation, usually dressed up as credits, or you can bill per unit of the customer's actual work. We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and we bill per property rather than per credit. Here is the reasoning, because it is a real tradeoff and not obviously the right call for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What per-credit optimizes for
&lt;/h2&gt;

&lt;p&gt;A credit is a unit of your cost, not the customer's value. When a tool bills per credit, the customer's incentive is to spend fewer credits, which means generating fewer images, which is the opposite of what makes their listing sell. Every render becomes a small yes-or-no budgeting decision. For a per-credit example with real, checked numbers, &lt;a href="https://casanovalabs.com/en/alternatives/roomagen" rel="noopener noreferrer"&gt;Roomagen&lt;/a&gt; bills by the credit and spends two credits per delivered image, which resolves to roughly $0.40 down to $0.23 per image depending on tier. The buyer's mental math is entirely at the image level: every render is a credit-to-image conversion done in your head before you click.&lt;/p&gt;

&lt;p&gt;That model is honest and works fine when usage is spiky and unpredictable. It maps cost to consumption, which is exactly what you want if a customer might run one image this month and four hundred the next. The downside is behavioral: it makes the customer ration the very thing your product is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What per-property optimizes for
&lt;/h2&gt;

&lt;p&gt;A property is a unit of the customer's work. A real estate agent does not think in renders, they think in listings. Billing per property, with a generous per-listing image allowance, changes the incentive: within a listing, generating one more staged room or one more sky replacement costs nothing marginal, so the agent does the thing that actually helps the sale. The unit of billing matches the unit of value.&lt;/p&gt;

&lt;p&gt;The cost to us of that model is variance. Some agents will use the full allowance on every listing, some a fraction, and we carry the spread rather than passing it through per render. That is a deliberate cost we take on to remove the per-image hesitation. The full grid and where the per-listing line lands is on the &lt;a href="https://casanovalabs.com/en/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, and how it compares against the per-credit tools is laid out in the &lt;a href="https://casanovalabs.com/en/alternatives" rel="noopener noreferrer"&gt;alternatives breakdown&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering consequence nobody mentions
&lt;/h2&gt;

&lt;p&gt;The billing unit leaks into the architecture. Per-credit billing forces a metering system: every operation is counted, reconciled, and shown to the user, and that counter is now on the critical path of every render. Per-property billing moves the accounting boundary up to the listing, so the render path itself carries no metering, which is simpler and faster and removes a class of "why was I charged for a failed generation" support tickets. The pricing model you pick is also a system-design decision, and the per-unit-of-value model happens to have the cleaner internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each is right
&lt;/h2&gt;

&lt;p&gt;Per-credit wins when consumption is genuinely unpredictable and the customer is technical enough to reason at the operation level, an API product for developers, say. Per-unit-of-value wins when the customer thinks in their own domain object and you want them to use the product freely rather than ration it. For real estate, the domain object is the listing, so that is the unit we bill. If you are designing a media SaaS, the question to answer first is not what to charge, it is what unit your customer already counts in, and whether your incentive should push them to do more of the thing or less.&lt;/p&gt;

&lt;p&gt;You can run the per-property model against ten photos without a card at &lt;a href="https://casanovalabs.com/en/login?mode=signup" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>pricing</category>
      <category>startup</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Phone Is Now the Camera and the Darkroom: What a Mobile Real Estate Photo Pipeline Actually Runs On-Device</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sun, 16 Aug 2026 20:44:21 +0000</pubDate>
      <link>https://dev.to/casanovalabs/the-phone-is-now-the-camera-and-the-darkroom-what-a-mobile-real-estate-photo-pipeline-actually-jlb</link>
      <guid>https://dev.to/casanovalabs/the-phone-is-now-the-camera-and-the-darkroom-what-a-mobile-real-estate-photo-pipeline-actually-jlb</guid>
      <description>&lt;p&gt;An agent walks a property with a phone, not a DSLR and a laptop. They want the listing photo finished before they are back in the car. That expectation quietly decides the architecture of any tool that serves them, because it forces a split: some of the work genuinely runs on the device, and some of it cannot, and pretending otherwise is where most mobile photo tools get slow or lie about latency.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing that people use from a phone browser, so this split is something we reason about constantly. Here is the honest map of what sits where.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the phone actually does well
&lt;/h2&gt;

&lt;p&gt;The capture side is entirely local, and modern phones are good at it. Exposure metering, multi-frame HDR merge, lens correction, and the preview you frame the shot with all run on the device silicon before a single byte leaves it. This is why the practical advice for shooting a listing on a phone is mostly about technique rather than gear: hold the phone level at chest height, use the main or ultra-wide lens rather than digital zoom, lock exposure so a bright window does not crush the room, and shoot in daylight. We wrote the full field version of that up in &lt;a href="https://casanovalabs.com/en/real-estate-photo-editing/real-estate-photos-with-iphone" rel="noopener noreferrer"&gt;how to take real estate photos with an iPhone&lt;/a&gt;, and none of it needs a server, because none of it is generative. It is optics and metering.&lt;/p&gt;

&lt;p&gt;The phone is also fine for the light edits: crop, straighten, a global exposure or white-balance nudge. Those are per-pixel operations with no model behind them, so they run in the browser on a canvas without a round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it has to leave the device
&lt;/h2&gt;

&lt;p&gt;The moment the operation is generative, the phone stops being enough. Replacing a grey sky, removing clutter from a room, or staging an empty space with furniture are all diffusion-model inferences. A single such pass is billions of floating-point operations against multi-gigabyte weights. That does not fit in a phone browser tab, and it should not: you do not want the model weights shipped to every device, and you do not want the battery and thermal cost of running them there.&lt;/p&gt;

&lt;p&gt;So the architecture that actually works is boring and correct: the phone captures and does the cheap edits locally, then hands the heavy generative step to a GPU on a server, and streams the result back. The user experience target is that the round trip feels like a filter, not like a render job. In practice a staging or replacement pass finishes in about 30 seconds on average, which is the number that decides whether an agent waits for it on-site or gives up and posts the raw photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The install question the App Store hides
&lt;/h2&gt;

&lt;p&gt;There is a second consequence of "from a phone" that is not about compute at all. A native app means an install, an update cycle, and a review queue, per platform. For a tool whose heavy work is server-side anyway, a large part of that install buys the user nothing, because the model was never going to run locally. A web app in the phone browser skips the install and keeps the same session open on a laptop later, which matches how agents actually work: shoot on the phone, tidy the batch on a desktop that evening.&lt;/p&gt;

&lt;p&gt;The tradeoff is real and worth stating plainly. Native still wins on camera-native capture flows and on true offline use. If you never have signal at the property, a web tool cannot do the generative step there, and a native app that caches a small on-device model could. We laid out that comparison in full, including what native does better, in &lt;a href="https://casanovalabs.com/en/articles/virtual-staging-app" rel="noopener noreferrer"&gt;the virtual staging app breakdown&lt;/a&gt;, and the same install-versus-browser logic applies to the broader category of &lt;a href="https://casanovalabs.com/en/articles/free-ai-interior-design-app" rel="noopener noreferrer"&gt;free AI interior design apps&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for anyone building this
&lt;/h2&gt;

&lt;p&gt;If you are architecting a mobile media tool, draw the line by asking one question per operation: is there a model in it? No model, keep it on the device and make it instant. Model in it, move it to a server and spend your engineering budget on making the round trip feel local. The mistake is trying to push generative work onto the phone to claim it is "on-device," which trades a 30-second server call for a slower, hotter, worse local one. The phone is the camera and the darkroom for everything that is optics. It is a thin client for everything that is inference. Building as if that line does not exist is how these tools end up slow.&lt;/p&gt;

&lt;p&gt;You can try the generative side of this on ten photos without a card at &lt;a href="https://casanovalabs.com/en/login?mode=signup" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>imageprocessing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Credit-to-Image Ratio Is the Number AI Tool Pricing Pages Bury</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Fri, 14 Aug 2026 05:07:14 +0000</pubDate>
      <link>https://dev.to/casanovalabs/the-credit-to-image-ratio-is-the-number-ai-tool-pricing-pages-bury-e1p</link>
      <guid>https://dev.to/casanovalabs/the-credit-to-image-ratio-is-the-number-ai-tool-pricing-pages-bury-e1p</guid>
      <description>&lt;p&gt;Credits are a pricing abstraction. They sit between the number a buyer reads on a plan card and the number that actually matters, which is what one finished, usable output costs. Most of the time the two are close enough that nobody checks. Sometimes they are not, and the gap is entirely legal, disclosed in a sentence most people skip.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, so we spend a lot of time reading competitors' pricing pages the way a buyer at volume would. This is the one normalization step that changes the ranking of tools more than any headline discount, and it takes about two minutes per tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits are not outputs
&lt;/h2&gt;

&lt;p&gt;A plan advertises a monthly credit balance. A separate line, usually further down, tells you how many credits one operation spends. The cost that matters is the second number divided into the first, then divided into the price.&lt;/p&gt;

&lt;p&gt;Here is a real grid. Roomagen, rates checked July 2026, lists a free tier at 6 credits, Starter at $12 a month for 60 credits, Pro at $29 for 200, and Business at $79 for 700. On its own that reads like roughly 60 to 700 units of value per tier. But one image spends two credits. So the delivered-image count per tier is 30, 100, and 350, and the real grid is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starter: $12 / 30 images = $0.40 per image&lt;/li&gt;
&lt;li&gt;Pro: $29 / 100 images = $0.29 per image&lt;/li&gt;
&lt;li&gt;Business: $79 / 350 images = about $0.23 per image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those numbers are hidden or improper. The two-credits-per-image rule is stated plainly. But the plan card shows you 60, 200, 700, and your brain anchors on those. The number you will actually budget against is 30, 100, 350.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;For any credit-based media tool, normalize before you compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cost_per_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan_price&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan_credits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;credits_per_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for every tier, not just the one you think you will buy, because the per-unit curve is where the tiers actually differ. Roomagen's curve above bends about 43% from smallest to largest tier, which is a genuinely tiered grid and, per unit, lands under a lot of the market including ours. A tool that charges one credit per output and a tool that charges two can advertise the same credit price and cost you double or half. The headline price tells you nothing until you divide it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the unit price still does not capture
&lt;/h2&gt;

&lt;p&gt;Cost per output is necessary, not sufficient. Once every tool is on the same per-output axis, three things decide the actual bill that the axis hides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The credit-to-output ratio can vary by operation.&lt;/strong&gt; One image at two credits is simple. A tool that spends two credits on a staging pass and five on a video clip has two different real prices under one balance. Normalize per operation type you will actually run, not once for the whole tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Coverage under one balance.&lt;/strong&gt; If photo and video draw from the same credits, the per-image number understates value, because the same budget also buys the more expensive operation. If they are separate products with separate balances, two cheap-looking plans can be one expensive stack. We put photo and video on one balance for exactly this reason, and it makes our per-image number look worse in isolation and better in a real month.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expiry.&lt;/strong&gt; Credits that reset monthly are a use-it-or-lose-it liability on anything with uneven volume, which real estate is. Rollover changes the effective price of every credit you do not burn. It rarely appears in the per-unit math and often dominates it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this belongs in an engineering conversation
&lt;/h2&gt;

&lt;p&gt;If you are building the tool, the ratio is a product decision with a trust cost: a two-to-one credit rule is defensible, a variable and unpredictable one trains buyers to distrust the balance. If you are buying, it is a procurement step you can script. Pull each competitor's plan price, credit balance, and credits-per-operation into three columns, apply the formula, and the tools re-sort themselves in front of you. The winner on the plan card is frequently not the winner on the third column.&lt;/p&gt;

&lt;p&gt;We wrote the full worked version of this, tier by tier and in each tool's own currency without inventing an exchange rate, as a direct comparison: &lt;a href="https://casanovalabs.com/en/alternatives/roomagen" rel="noopener noreferrer"&gt;CasaNova Labs versus Roomagen for listing photos&lt;/a&gt;. It says plainly where the per-image price goes the other way, because on that axis it does. If you want to run the same math against our own grid, the &lt;a href="https://casanovalabs.com/en/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; lists every tier and the free trial is 10 transformations with no card, where one transformation is one delivered image.&lt;/p&gt;

&lt;p&gt;The point is not that any one tool wins. It is that the number on the plan card is the wrong number, and the right one is two minutes of division away.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>pricing</category>
      <category>startup</category>
    </item>
    <item>
      <title>What One AI-Staged Room Actually Costs to Render: The GPU Math Behind Per-Image Pricing</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:42:01 +0000</pubDate>
      <link>https://dev.to/casanovalabs/what-one-ai-staged-room-actually-costs-to-render-the-gpu-math-behind-per-image-pricing-52hh</link>
      <guid>https://dev.to/casanovalabs/what-one-ai-staged-room-actually-costs-to-render-the-gpu-math-behind-per-image-pricing-52hh</guid>
      <description>&lt;p&gt;If you have run a diffusion model locally, the marginal cost of one image feels like zero. The model is already in memory, the GPU is already warm, and you just call &lt;code&gt;pipe(prompt)&lt;/code&gt; again. That intuition is what makes teams underestimate the real number when they move the same feature into a product. The per-image cost in production is dominated by things the notebook hides: the seconds you pay for a GPU that is loading a model, the VRAM you rent whether or not a request is in flight, and the renders you throw away.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate imagery, so this arithmetic is our unit economics. Here is how it actually breaks down, with numbers you can plug your own rates into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model is bigger than people remember
&lt;/h2&gt;

&lt;p&gt;The workhorse for photoreal room generation is an SDXL-class latent diffusion model. Per the &lt;a href="https://arxiv.org/abs/2307.01952" rel="noopener noreferrer"&gt;SDXL technical report&lt;/a&gt;, its UNet has around 2.6 billion parameters, roughly three times the UNet of Stable Diffusion 1.5, and it renders natively at 1024x1024. In half precision that weight sits at about 10 GB of VRAM before you add anything, and a real staging pipeline adds plenty: a ControlNet to keep the room geometry, an inpainting pass to swap furniture without touching the walls, sometimes an upscaler on top.&lt;/p&gt;

&lt;p&gt;So the first cost is not compute, it is the size of the box you have to rent. You cannot serve a 10 to 14 GB working set on a small GPU. You are renting an A10G, an L4, or an A100-class card, and you are renting it for the whole time the process is alive, not only while a request runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cold starts are the line item nobody budgets for
&lt;/h2&gt;

&lt;p&gt;Loading a multi-gigabyte checkpoint from disk into VRAM is not instant. Depending on where the weights live and how fast the storage is, moving 10-plus GB into the GPU takes on the order of seconds to tens of seconds. If you run serverless GPU inference and let workers scale to zero to save money, every request that lands on a cold worker pays that penalty before it renders a single pixel.&lt;/p&gt;

&lt;p&gt;This is the tradeoff that quietly sets your cost floor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep a worker warm and you pay for idle GPU time between requests.&lt;/li&gt;
&lt;li&gt;Let it scale to zero and your tail latency spikes, and you burn GPU seconds re-loading the model instead of rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither option is free, and the cheaper one depends entirely on your request pattern. Bursty, unpredictable traffic (which is exactly what a listing-editing tool sees) is the worst case for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic, with a rate you can change
&lt;/h2&gt;

&lt;p&gt;Take the compute itself. A 1024x1024 SDXL render at around 30 steps is a few seconds of GPU time on a mid-range card, call it 4 seconds for a round number, more with ControlNet and an upscale pass. Now assume a GPU that costs 2 dollars an hour. That is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 dollars / 3600 seconds = 0.00056 dollars per GPU-second
4 seconds x 0.00056        = 0.0022 dollars of pure compute per image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fifth of a cent. That is the number that makes AI rendering look almost free, and it is the number demos quote. It is also fiction as a unit cost, because it ignores everything around the render:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold-start seconds amortized across real, bursty traffic.&lt;/li&gt;
&lt;li&gt;Idle GPU time when you keep workers warm to hide those cold starts.&lt;/li&gt;
&lt;li&gt;Retries and rejects. Diffusion is stochastic; some fraction of outputs miss and get re-rolled, and you pay full compute for images a human never keeps.&lt;/li&gt;
&lt;li&gt;Storage and egress for the input photo, the output, and usually several intermediate variants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on how warm you run and how often you retry, the real cost per delivered image can land several times higher than the raw compute line. The exact multiple is the entire engineering problem, and it is why serious inference platforms bill by the GPU-second rather than by the image: the per-image number is not stable enough to quote until you have absorbed all of the above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this maps onto build versus buy
&lt;/h2&gt;

&lt;p&gt;If you are adding an AI image feature to your own product, this is the honest tradeoff. Running it yourself means you own the warm-pool tuning, the autoscaling, the retry logic, and a GPU bill that is mostly idle time and cold starts rather than the four seconds of actual rendering. That is real, ongoing engineering, not a weekend integration.&lt;/p&gt;

&lt;p&gt;The alternative is to let someone who has already amortized that infrastructure across many tenants absorb it, and pay a flat, predictable number per delivered image. That predictability is the product. It is why our &lt;a href="https://casanovalabs.com/en/pricing" rel="noopener noreferrer"&gt;per-image pricing&lt;/a&gt; is a fixed figure per render rather than a metered GPU bill you cannot forecast, and why the &lt;a href="https://casanovalabs.com/en/virtual-staging" rel="noopener noreferrer"&gt;virtual staging&lt;/a&gt; itself is priced by the finished photo, not the compute behind it.&lt;/p&gt;

&lt;p&gt;The takeaway is not that one path always wins. It is that the demo number, a fifth of a cent, is never the number you ship on. Before you decide to run diffusion inference in-house, price the cold starts and the idle time, not the render. That is where the cost actually lives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Why a 30-Second Real Estate Video Can Be 8 MB or 80 MB: The ffmpeg Settings That Decide</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:35:18 +0000</pubDate>
      <link>https://dev.to/casanovalabs/why-a-30-second-real-estate-video-can-be-8-mb-or-80-mb-the-ffmpeg-settings-that-decide-13fn</link>
      <guid>https://dev.to/casanovalabs/why-a-30-second-real-estate-video-can-be-8-mb-or-80-mb-the-ffmpeg-settings-that-decide-13fn</guid>
      <description>&lt;p&gt;Two 30-second walkthrough clips, same resolution, same footage quality, can land at 8 MB and 80 MB out of the same encoder. The gap is not the camera or the content. It is three settings in the &lt;code&gt;ffmpeg&lt;/code&gt;/&lt;code&gt;libx264&lt;/code&gt; invocation, all documented in FFmpeg's own H.264 encoding guide.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and every video the product renders goes through this same re-encode step before it reaches a listing page. Here is what actually drives the size, straight from the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  CRF, not bitrate, is the setting that controls quality
&lt;/h2&gt;

&lt;p&gt;FFmpeg's guide recommends Constant Rate Factor as "the recommended rate control mode for most uses" when you care about consistent quality over a guaranteed file size. The scale runs 0 to 51 for 8-bit video: 0 is lossless, 23 is the default, 51 is the worst quality possible. The docs describe "a subjectively sane range" of 17 to 28, with 17 and 18 as "visually lossless or nearly so."&lt;/p&gt;

&lt;p&gt;The part that explains the 8 MB against 80 MB swing directly: "the range is exponential, so increasing the CRF value +6 results in roughly half the bitrate / file size, while -6 leads to roughly twice the bitrate." Applied to the documented sane range, moving from CRF 18 to CRF 28, a 10-point difference, works out to roughly a threefold difference in bitrate for identical footage, using that same exponential relationship. That is before touching resolution, frame rate or anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just target a fixed file size instead
&lt;/h2&gt;

&lt;p&gt;CRF mode is the documented default recommendation, but the guide is upfront about what it gives up to get there: "you can't tell it to get a specific filesize or not go over a specific size or bitrate, which means that this method is not recommended for encoding videos for streaming" in the sense of guaranteeing a bitrate cap. For a walkthrough clip served once from a listing page rather than adaptively streamed, that tradeoff runs the other way: a consistent visual quality at whatever size the footage needs is the more useful property than hitting an arbitrary size target and letting quality float.&lt;/p&gt;

&lt;p&gt;That tradeoff is also where the cost sits, because bytes served and encoder minutes are both billed. We broke down what that adds up to per listing in our &lt;a href="https://casanovalabs.com/en/articles/virtual-staging-cost" rel="noopener noreferrer"&gt;breakdown of virtual staging costs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preset trades encoding time for file size, not quality
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;preset&lt;/code&gt; is, per the docs, "a collection of options that will provide a certain encoding speed to compression ratio." At a fixed CRF, a slower preset produces a smaller file for the same visual quality, because the encoder spends more time finding a more efficient way to represent the same picture. The documented list, fastest to slowest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ultrafast, superfast, veryfast, faster, fast, medium (default), slow, slower, veryslow, placebo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;placebo&lt;/code&gt; is explicitly called out as "not useful" in the docs' own FAQ. The stated rule of thumb: "use the slowest preset that you have patience for." A worked example from the guide, targeting good quality with better compression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; slow &lt;span class="nt"&gt;-crf&lt;/span&gt; 22 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mkv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-c:a copy&lt;/code&gt; there stream-copies the audio track instead of re-encoding it. There is no reason to spend CPU re-compressing audio that is not being touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;faststart&lt;/code&gt; is one flag, and it decides whether playback waits for the whole file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-movflags +faststart&lt;/code&gt; moves part of an MP4's metadata to the front of the file. FFmpeg's guide is direct about why it matters for the web specifically: it "will allow the video to begin playing before it is completely downloaded by the viewer," and it is not required for a platform like YouTube because "they can begin re-encoding before uploads complete" on their end. That distinction matters if a video is being served directly rather than re-processed by a hosting platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting the numbers together
&lt;/h2&gt;

&lt;p&gt;None of ultrafast/CRF 18, slow/CRF 23, or veryslow/CRF 28 will look dramatically different at a glance on a 30-second walkthrough. But per FFmpeg's own exponential CRF relationship, the gap between the low and high end of the sane range alone is close to threefold, before preset compounds it further and before faststart decides whether that file streams cleanly or stalls on the first frame. That is the trade CasaNova Labs tunes on the server side for every video the platform renders, so a walkthrough clip lands at a size a listing page can actually serve, instead of whatever preset and CRF a default &lt;code&gt;ffmpeg&lt;/code&gt; invocation happened to pick.&lt;/p&gt;

&lt;p&gt;If you are building the generation side of a video pipeline yourself, these three settings are most of what a 30-second clip's file size comes down to. &lt;a href="https://casanovalabs.com/en/real-estate-photo-editing" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; is where we apply them to real estate listings specifically.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Serving AI-Generated Images at Scale on Next.js: Formats, Cache and the App Router Traps</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:58:59 +0000</pubDate>
      <link>https://dev.to/casanovalabs/serving-ai-generated-images-at-scale-on-nextjs-formats-cache-and-the-app-router-traps-19c6</link>
      <guid>https://dev.to/casanovalabs/serving-ai-generated-images-at-scale-on-nextjs-formats-cache-and-the-app-router-traps-19c6</guid>
      <description>&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; is built for a mostly-static image set: a handful of hero shots, a product catalogue, a blog's cover images. Point it at a pipeline that generates a fresh image per user action — an edited listing photo, a new render — and a few defaults that are invisible in the common case start to matter.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and every output the product generates gets served back through exactly this kind of pipeline. Here is what Next.js's own reference docs say about the parts that actually bite at that volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format negotiation runs off the Accept header, not a version check
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;formats&lt;/code&gt; config controls which encodings the optimizer is allowed to produce. The default is &lt;code&gt;['image/webp']&lt;/code&gt;; Next.js reads the request's &lt;code&gt;Accept&lt;/code&gt; header to pick the best match, and if more than one configured format matches, "the array order matters" — the first hit in the array wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/avif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are specific about the cost of adding AVIF: it "generally takes 50% longer to encode but it compresses 20% smaller compared to WebP," and when multiple formats are configured, "Next.js will cache each format separately" — meaning every source image now produces multiple cached variants, not one. Worth deciding on purpose, not by copying a starter config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache TTL is a floor, not a fact about the file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;minimumCacheTTL&lt;/code&gt; sets the minimum seconds an optimized image is cached for; the default is &lt;code&gt;14400&lt;/code&gt; (4 hours). The actual expiration is "defined by either the &lt;code&gt;minimumCacheTTL&lt;/code&gt; or the upstream image &lt;code&gt;Cache-Control&lt;/code&gt; header, whichever is larger" — so a slow-changing config value can still be overridden upward by whatever header the origin image sent.&lt;/p&gt;

&lt;p&gt;The sharper edge is what happens when a source image changes: "there is no mechanism to invalidate the cache at this time." The documented fix is to change the &lt;code&gt;src&lt;/code&gt; prop (a new URL is a new cache entry) or delete the cached file under &lt;code&gt;&amp;lt;distDir&amp;gt;/cache/images&lt;/code&gt;. For anything that regenerates the same nominal image — a re-edited photo served from the same path — that's the detail that decides whether users see the old version for up to &lt;code&gt;minimumCacheTTL&lt;/code&gt; seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static imports skip the whole caching question
&lt;/h2&gt;

&lt;p&gt;For images that ship with the build rather than being generated at request time, the docs point at a different mechanism entirely: a Static Image Import. Importing a file directly, &lt;code&gt;import hero from './hero.jpg'&lt;/code&gt;, makes Next.js "automatically hash the file contents and cache the image forever with a &lt;code&gt;Cache-Control&lt;/code&gt; header of &lt;code&gt;immutable&lt;/code&gt;" — no TTL to tune, no invalidation problem, because a content hash in the URL means a changed file is a new URL. That only applies to build-time assets; anything generated after a request comes in still goes through the &lt;code&gt;remotePatterns&lt;/code&gt; and &lt;code&gt;minimumCacheTTL&lt;/code&gt; path above. Worth knowing which of the two a given image actually is before reaching for cache config.&lt;/p&gt;

&lt;p&gt;There's a related edge worth checking before wiring up authenticated image sources: "the Image Optimization API using the default loader will &lt;em&gt;not&lt;/em&gt; forward headers when fetching the &lt;code&gt;src&lt;/code&gt; image." A signed or authenticated URL for a freshly generated image can fail silently through the optimizer for exactly this reason — the documented way around it is the &lt;code&gt;unoptimized&lt;/code&gt; prop, which serves the source as-is instead of routing it through &lt;code&gt;/_next/image&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every external source has to be allow-listed
&lt;/h2&gt;

&lt;p&gt;If the images being served don't live under &lt;code&gt;/public&lt;/code&gt;, &lt;code&gt;remotePatterns&lt;/code&gt; is mandatory — Next.js will 400 anything that doesn't match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/renders/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;domains&lt;/code&gt;, the older config, is deprecated since Next.js 14 specifically because it "does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname" — for a storage bucket or CDN host serving generated output, that's a meaningfully wider allow surface than &lt;code&gt;remotePatterns&lt;/code&gt; needs to grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;sizes&lt;/code&gt; decides how much of the srcset gets built
&lt;/h2&gt;

&lt;p&gt;Without a &lt;code&gt;sizes&lt;/code&gt; prop, "the browser assumes the image will be as wide as the viewport (100vw)," which the docs call out directly as a cause of unnecessarily large downloads. &lt;code&gt;sizes&lt;/code&gt; also changes what Next.js generates on the backend: without it, a limited srcset (roughly 1x/2x); with it, a full breakpoint-based srcset. For a grid of AI-generated thumbnails at a fixed card width, an unset &lt;code&gt;sizes&lt;/code&gt; prop is a real bandwidth cost, not a cosmetic detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quality allowlist is enforced server-side
&lt;/h2&gt;

&lt;p&gt;As of Next.js 16, &lt;code&gt;qualities&lt;/code&gt; is a required config — an explicit allowlist of quality values the optimizer will produce, defaulting to &lt;code&gt;[75]&lt;/code&gt;. A &lt;code&gt;quality&lt;/code&gt; prop that doesn't match an allowed value snaps to the closest one; hitting the optimization endpoint directly with a disallowed value returns &lt;code&gt;400&lt;/code&gt;. It exists, per the docs, because "unrestricted access could allow malicious actors to optimize more qualities than intended" — worth setting deliberately once output is coming from more than one static list of assets.&lt;/p&gt;

&lt;p&gt;None of these are bugs in Next.js — they're defaults tuned for a mostly-static image set, documented plainly, and worth revisiting the moment a pipeline starts generating images instead of just storing them. That's exactly the case &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; builds for on every listing photo it renders back to a browser.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What a Portal's Resize Pipeline Actually Does to a Real Estate Listing Photo</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:58:58 +0000</pubDate>
      <link>https://dev.to/casanovalabs/what-a-portals-resize-pipeline-actually-does-to-a-real-estate-listing-photo-17l3</link>
      <guid>https://dev.to/casanovalabs/what-a-portals-resize-pipeline-actually-does-to-a-real-estate-listing-photo-17l3</guid>
      <description>&lt;p&gt;A listing photo leaves the camera looking right. By the time a portal has resized it for a thumbnail grid and recompressed it for delivery, it can look flatter, softer, or faintly wrong in a way nobody can quite name. None of that requires a bug on anyone's part — every step is documented, default behaviour in the image libraries doing the work.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and the same three operations — resize, recompress, convert format — run on every image the product touches. Here is what the defaults in &lt;code&gt;sharp&lt;/code&gt; (the Node.js binding for &lt;code&gt;libvips&lt;/code&gt;, the library most JS image pipelines end up calling) actually do at each step, read from sharp's own API docs rather than assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kernel decides what a downscale looks like
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resize()&lt;/code&gt; takes a &lt;code&gt;kernel&lt;/code&gt; option — the algorithm used to compute new pixel values when shrinking an image, with the inferred interpolator reused for upsampling. The default is &lt;code&gt;lanczos3&lt;/code&gt;, not the linear or nearest-neighbour interpolation a naive implementation might reach for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nearest&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is in sharp's own docs as an example of what nearest-neighbour resizing looks like — a hard "before" case for comparison, not a recommendation. The docs also flag a second, easy-to-miss knob: &lt;code&gt;fastShrinkOnLoad&lt;/code&gt;, on by default, which takes advantage of JPEG and WebP's built-in shrink-on-load feature for speed. The tradeoff is explicit in the reference: it "can lead to a slight moiré pattern or round-down of an auto-scaled dimension." Fine for a thumbnail grid, worth turning off for a hero image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chroma subsampling isn't the same default across formats
&lt;/h2&gt;

&lt;p&gt;Chroma subsampling throws away colour resolution the eye is less sensitive to while keeping full luminance detail. Sharp's &lt;code&gt;jpeg()&lt;/code&gt; output defaults to &lt;code&gt;'4:2:0'&lt;/code&gt; — three-quarters of the colour data discarded — unless you set it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;chromaSubsampling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4:4:4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part worth checking rather than assuming: &lt;code&gt;avif()&lt;/code&gt; and &lt;code&gt;heif()&lt;/code&gt; default the other way, to &lt;code&gt;'4:4:4'&lt;/code&gt; (full chroma), and only drop to &lt;code&gt;'4:2:0'&lt;/code&gt; if you ask for it. &lt;code&gt;webp()&lt;/code&gt; sits in between, with a &lt;code&gt;smartSubsample&lt;/code&gt; flag for higher-quality chroma subsampling and an &lt;code&gt;effort&lt;/code&gt; scale from &lt;code&gt;0&lt;/code&gt; (fastest) to &lt;code&gt;6&lt;/code&gt; (slowest), default &lt;code&gt;4&lt;/code&gt;. Three formats, three different defaults for the same concept — a pipeline that hard-codes one assumption across all three will be right for one of them and wrong for the other two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The colour profile gets stripped unless you say otherwise
&lt;/h2&gt;

&lt;p&gt;This is the default sharp documents most plainly, and the one most likely to surprise: "the default behaviour, when &lt;code&gt;keepMetadata&lt;/code&gt; is not used, is to convert to the device-independent sRGB colour space and strip all metadata, including the removal of any ICC profile." A photo shot in Adobe RGB or with a camera-embedded profile gets flattened to sRGB on the way out, silently, every time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withMetadata()&lt;/code&gt; is the documented opt-out — it keeps EXIF, XMP and IPTC, and "will also convert to and add a web-friendly sRGB ICC profile if appropriate." For a specific target profile rather than the automatic one, &lt;code&gt;withIccProfile()&lt;/code&gt; takes either a filesystem path or one of the built-in names: &lt;code&gt;srgb&lt;/code&gt;, &lt;code&gt;p3&lt;/code&gt;, &lt;code&gt;cmyk&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;withIccProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cropping for a grid uses the same defaults, silently
&lt;/h2&gt;

&lt;p&gt;A portal's thumbnail grid almost always crops rather than letterboxes, and that crop has its own default worth knowing. Sharp's &lt;code&gt;resize()&lt;/code&gt; takes a &lt;code&gt;fit&lt;/code&gt; option — &lt;code&gt;cover&lt;/code&gt; (default), &lt;code&gt;contain&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;inside&lt;/code&gt; or &lt;code&gt;outside&lt;/code&gt; — modelled on the CSS &lt;code&gt;object-fit&lt;/code&gt; property. With &lt;code&gt;fit: 'cover'&lt;/code&gt;, the crop is centred by default (&lt;code&gt;position: 'centre'&lt;/code&gt;), but it doesn't have to guess blindly: sharp also documents &lt;code&gt;gravity&lt;/code&gt; values (&lt;code&gt;north&lt;/code&gt;, &lt;code&gt;northeast&lt;/code&gt;, … &lt;code&gt;centre&lt;/code&gt;) and two content-aware strategies, &lt;code&gt;entropy&lt;/code&gt; (crops toward the region with the highest Shannon entropy) and &lt;code&gt;attention&lt;/code&gt; (crops toward the region of highest luminance frequency). A pipeline that leaves &lt;code&gt;fit&lt;/code&gt; and &lt;code&gt;position&lt;/code&gt; at their defaults gets a centred crop every time — fine for a portrait already centred on the subject, wrong for a wide shot where the interesting part of the room sits off to one side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attention&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thumbnail.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  None of this throws an error
&lt;/h2&gt;

&lt;p&gt;That is the actual problem with all three defaults: they are not bugs, so nothing fails and nothing logs. The image resizes, the format converts, the file ships — at three-quarters colour resolution, flattened to sRGB, through a kernel nobody chose on purpose. A pipeline that cares about the output has to set &lt;code&gt;kernel&lt;/code&gt;, &lt;code&gt;chromaSubsampling&lt;/code&gt; and the metadata/ICC handling deliberately, per format, rather than accept whatever the library shipped as a sane general-purpose default.&lt;/p&gt;

&lt;p&gt;That is the layer we tune for every image &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; outputs, so a listing photo that comes out of the pipeline keeps the colour fidelity it went in with, rather than whatever three unrelated defaults happen to agree on.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>sharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>EU AI Act Article 50 for Image and Video Pipelines: What Actually Has to Be in the File</title>
      <dc:creator>casanovalabs</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:45:58 +0000</pubDate>
      <link>https://dev.to/casanovalabs/eu-ai-act-article-50-for-image-and-video-pipelines-what-actually-has-to-be-in-the-file-12f1</link>
      <guid>https://dev.to/casanovalabs/eu-ai-act-article-50-for-image-and-video-pipelines-what-actually-has-to-be-in-the-file-12f1</guid>
      <description>&lt;p&gt;Article 50 of the EU AI Act became applicable on 2 August 2026. It is one of the few provisions in the regulation that turns into an engineering ticket rather than a policy document: a system that generates images, audio or video must produce outputs that an automated tool can detect as artificially generated.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt;, an AI studio for real estate photo and video editing, and we shipped this ahead of the date across every image and every video the product outputs. What follows isn't a legal explainer — it's what the requirement means at the file level, and the three traps that will make a pipeline ship unmarked files while every test stays green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two obligations, two different actors
&lt;/h2&gt;

&lt;p&gt;The single most useful thing to get right before writing any code: Article 50 bundles requirements that fall on &lt;strong&gt;different parties&lt;/strong&gt;, and they are satisfied by different mechanisms.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;§2&lt;/strong&gt; binds the &lt;strong&gt;provider&lt;/strong&gt; of the generative system. Every synthetic output must carry marking in a &lt;strong&gt;machine-readable&lt;/strong&gt; format. Nothing in it is about human visibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;§4&lt;/strong&gt; binds the &lt;strong&gt;deployer&lt;/strong&gt; — whoever publishes the content. Where the content qualifies as a deepfake under Art. 3(60), manipulated content resembling existing people, objects or &lt;strong&gt;places&lt;/strong&gt; in a way that could pass as authentic, it must be disclosed "in a clear and recognisable manner."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction has a direct architectural consequence: §2 is metadata a detector parses, §4 is a label a human reads. They are two separate pipelines with two separate failure modes, and conflating them is the most common design mistake we've seen discussed.&lt;/p&gt;

&lt;p&gt;For our part, the claim we make and the only one: &lt;strong&gt;our outputs carry machine-readable marking within the meaning of Article 50(2)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "machine-readable" resolves to
&lt;/h2&gt;

&lt;p&gt;There is no bespoke format to invent. Detectors converge on the IPTC &lt;code&gt;DigitalSourceType&lt;/code&gt; property, and the controlled-vocabulary value that matters for generated content is &lt;code&gt;trainedAlgorithmicMedia&lt;/code&gt;. Written into an XMP packet, it is what an automated checker actually looks for — a string a parser can match, rather than a free-text note in a description field that no tool will ever read.&lt;/p&gt;

&lt;p&gt;For still images that packet sits alongside standard EXIF fields. For MP4, it goes into a top-level &lt;code&gt;uuid&lt;/code&gt; box carrying the UUID reserved for XMP by the XMP specification (Part 3, &lt;em&gt;Storage in Files&lt;/em&gt;) — no re-encode required, since an MP4 is a flat sequence of &lt;code&gt;[size][type][payload]&lt;/code&gt; boxes.&lt;/p&gt;

&lt;p&gt;So far, so mechanical. The interesting part is everything that quietly removes it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1 — your own resize eats the metadata
&lt;/h2&gt;

&lt;p&gt;Most image processing libraries drop metadata on transform by default; it's the safe behaviour for them and the wrong one for you. A thumbnail route that resizes on the fly will serve a file with no marking whatsoever, no matter what the stored original carries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// strips EXIF/XMP — the thumbnail ships unmarked&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// preserves it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;withMetadata&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing throws. The image looks perfect. &lt;strong&gt;Audit every path that re-encodes an image between storage and the client&lt;/strong&gt;, not just the one that writes it — delivery-time transforms are where this hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2 — in MP4, &lt;em&gt;where&lt;/em&gt; you write it decides whether the file still plays
&lt;/h2&gt;

&lt;p&gt;Appending a box to an MP4 is only safe at the end. The &lt;code&gt;moov&lt;/code&gt; atom's &lt;code&gt;stco&lt;/code&gt; table holds &lt;strong&gt;absolute byte offsets&lt;/strong&gt; into &lt;code&gt;mdat&lt;/code&gt;: insert anything before &lt;code&gt;mdat&lt;/code&gt; and every one of those offsets is now wrong. This is not theoretical — a box inserted right after &lt;code&gt;ftyp&lt;/code&gt; gives a file that decodes zero frames with &lt;code&gt;Invalid NAL unit size&lt;/code&gt;. Appended after the last box, nothing shifts and playback is untouched.&lt;/p&gt;

&lt;p&gt;If you take one thing from this article: append, never insert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3 — the visible label destroys the machine marking
&lt;/h2&gt;

&lt;p&gt;This one only appears once both obligations are implemented, which is why it tends to be found late.&lt;/p&gt;

&lt;p&gt;A §4 visible label on video means overlaying a rendered label on every frame, which means a full re-encode. And a re-encode &lt;strong&gt;rebuilds the container from scratch&lt;/strong&gt; — wiping out any &lt;code&gt;uuid&lt;/code&gt; box you appended beforehand. Run the two steps in the intuitive order (machine marking first, since it's the cheap one) and the §2 marking silently disappears from every single video you ship.&lt;/p&gt;

&lt;p&gt;The correct order is the counter-intuitive one: &lt;strong&gt;visible mark first, machine marking last.&lt;/strong&gt; Whatever writes to the container has to be the final operation in the chain.&lt;/p&gt;

&lt;p&gt;There's a broader version of this rule worth internalising: the marking step belongs at the single choke point every output passes through on its way to storage, and it belongs &lt;em&gt;after&lt;/em&gt; every transform. Anything that runs later gets to undo it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Article 50(2) is a small, mechanical requirement once you separate it cleanly from §4: write a controlled-vocabulary metadata field, in a format that survives your own transforms, at the one point every output funnels through. The whole difficulty is in ordering — and none of the three traps above produce an error, a log line or a failing test. They produce a file that looks correct and isn't marked.&lt;/p&gt;

&lt;p&gt;We shipped this across every image and video &lt;a href="https://casanovalabs.com" rel="noopener noreferrer"&gt;CasaNova Labs&lt;/a&gt; produces, ahead of the 2 August 2026 date. If you're building on the generation side, the three ordering rules above are what the requirement actually costs you: an afternoon, and knowing where to look.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>node</category>
      <category>webdev</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
