<?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: tony huang</title>
    <description>The latest articles on DEV Community by tony huang (@tonytoolbox).</description>
    <link>https://dev.to/tonytoolbox</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%2F4151487%2Fdfb79ed6-2aa5-4e19-af56-44c18f810be1.png</url>
      <title>DEV Community: tony huang</title>
      <link>https://dev.to/tonytoolbox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonytoolbox"/>
    <language>en</language>
    <item>
      <title>Local-first photo editing in the browser: what the pipeline split actually looks like</title>
      <dc:creator>tony huang</dc:creator>
      <pubDate>Mon, 19 Oct 2026 19:08:19 +0000</pubDate>
      <link>https://dev.to/tonytoolbox/local-first-photo-editing-in-the-browser-what-the-pipeline-split-actually-looks-like-kp</link>
      <guid>https://dev.to/tonytoolbox/local-first-photo-editing-in-the-browser-what-the-pipeline-split-actually-looks-like-kp</guid>
      <description>&lt;p&gt;If you build anything in the browser that touches user photos, you eventually run into the same wall: the cheap operations want to be local, and the expensive ones cannot be.&lt;/p&gt;

&lt;p&gt;Here is how I ended up splitting the pipeline, and the constraints that forced each decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost asymmetry nobody mentions in the docs
&lt;/h2&gt;

&lt;p&gt;A brightness slider is a loop over pixels. A crop is a coordinate transform. A JPEG re-encode is a codec call. None of these need a server, and if you put them behind one you have added latency, bandwidth and a privacy problem to something that costs you nothing to run locally.&lt;/p&gt;

&lt;p&gt;A diffusion step is different. It needs a GPU, it costs real money per call, and there is no version of it that runs acceptably in a browser tab today. That is not a temporary limitation of my code — it is the state of the hardware.&lt;/p&gt;

&lt;p&gt;So the pipeline naturally splits: everything deterministic runs in the client, everything generative runs on a server, and the boundary between them is the honest place to draw your pricing model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the local layer actually costs
&lt;/h2&gt;

&lt;p&gt;Canvas-based operations are fast until they are not. The three things that bit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;getImageData&lt;/code&gt; is a synchronous trap.&lt;/strong&gt; Every pixel-level pass that reads back from the canvas stalls the main thread while the GPU drains. On a 4000×3000 photo that is a visible freeze. The fix is to do the pixel work once per adjustment change, not once per frame, and to debounce slider input — otherwise you are running the full loop on every mousemove event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Re-encoding is the real cost, not the filter.&lt;/strong&gt; Applying a filter is milliseconds; encoding the result to JPEG at quality 0.9 to show a preview is where the time goes. Previewing at display resolution and only re-encoding at full resolution on export makes the interaction feel instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Memory is worse than you think.&lt;/strong&gt; An RGBA buffer for a 12-megapixel photo is 48 MB. Keep a source copy, a working copy and an undo stack and you are at a few hundred megabytes on a phone. Mobile Safari will kill the tab without ceremony. Downscaling the working copy to the viewport size and keeping the original as a compressed blob is the only thing that survived real testing.&lt;/p&gt;

&lt;p&gt;None of that is exotic, but it means "local-first" is an architectural commitment rather than a checkbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The network panel is the honest test
&lt;/h2&gt;

&lt;p&gt;The nice side effect of this split is that it is verifiable from outside. Open DevTools, switch to the Network tab, and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply a tone adjustment. Requests appearing there means the claim is false.&lt;/li&gt;
&lt;li&gt;Use the generative feature. Requests are expected — that part really does cost money per call.&lt;/li&gt;
&lt;li&gt;Export. If the only difference between the free and paid path is this step, you have found the business gate, which is a separate thing from a technical limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tool that passes the first check and is open about the second is telling you the truth about its own cost structure. That is a better signal than any badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy argument is a side effect
&lt;/h2&gt;

&lt;p&gt;I did not start with a privacy pitch. But once the local layer exists, the useful consequence follows: photos that never leave the device are photos people are willing to use. Scans of documents, pictures of kids, product shots under NDA — the category that gets excluded from every cloud editor by default becomes available.&lt;/p&gt;

&lt;p&gt;That is a real product difference, and it comes from the architecture, not from a policy page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the split breaks
&lt;/h2&gt;

&lt;p&gt;Two honest limits, because anyone claiming otherwise is selling something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebAssembly models are not a shortcut.&lt;/strong&gt; You can run a small model in the browser, and people do. But it is slow enough to be unusable for anything interactive on a laptop, and it drains the battery doing it. For the operations people actually want (restoration, background removal, upscaling, style generation) the server is the only option that produces good output in reasonable time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The boundary moves.&lt;/strong&gt; Every year a model that needed a GPU gets smaller. The split I have today will be wrong in a couple of years, and rebuilding around a new boundary is cheaper than pretending the old one is permanent.&lt;/p&gt;




&lt;p&gt;I build &lt;a href="https://aiphotoeditor.best/" rel="noopener noreferrer"&gt;aiphotoeditor.best&lt;/a&gt; along these lines: the local layer (tone, filters, crop, resize, format conversion, compression) is free and unlimited because it costs nothing to run; the AI layer is capped at three free edits a day and then one-time credit packs, because each call costs money. If you want to check the split for yourself, the network panel test above works on it too.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>privacy</category>
    </item>
    <item>
      <title>What it costs to run an AI baby-photo generator: ~4¢ an image, and an honest promise</title>
      <dc:creator>tony huang</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:00:10 +0000</pubDate>
      <link>https://dev.to/tonytoolbox/what-it-costs-to-run-an-ai-baby-photo-generator-4c-an-image-and-an-honest-promise-54oe</link>
      <guid>https://dev.to/tonytoolbox/what-it-costs-to-run-an-ai-baby-photo-generator-4c-an-image-and-an-honest-promise-54oe</guid>
      <description>&lt;p&gt;I build small browser-first tools. One of them, &lt;a href="https://ourbaby.shop/" rel="noopener noreferrer"&gt;OurBaby&lt;/a&gt;, answers the oldest question couples ask — &lt;em&gt;what will our baby look like?&lt;/em&gt; — by blending one photo of each parent into an imagined baby portrait.&lt;/p&gt;

&lt;p&gt;Two questions come up from other developers every time I describe it, so here are both answers with real numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit economics (real numbers, October 2026)
&lt;/h2&gt;

&lt;p&gt;Generation runs on a multi-image model of the "nano-banana" class via fal.ai. The actual billed cost per image, from my fal usage export:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.0398 per generated image&lt;/strong&gt; (identity-blending edit, two reference photos)&lt;/li&gt;
&lt;li&gt;Same engine for every scene — studio, Christmas, beach, family-of-three&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the math is simple: a $2.99 pack of 5 photos is ~80%+ gross margin, and the free tier is a bounded acquisition cost rather than a leak. The free tier is deliberately small and capped three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;1 free preview per visitor IP per day&lt;/li&gt;
&lt;li&gt;a per-site daily spend circuit breaker (cost-estimate accumulation in KV)&lt;/li&gt;
&lt;li&gt;no API key in the deployment → the endpoint returns a waitlist 503 instead of ever going bare&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one matters. An earlier project of mine shipped an AI endpoint without a daily cap and someone found it within hours — the cap is the difference between a marketing cost and an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Photos never touch my servers as files: the browser downscales each to a 768px JPEG, sends them as data URIs, and a Cloudflare Pages Function makes a single queued call to the model with both references. The interesting part is not the plumbing, it's the prompt: the model is told to &lt;em&gt;inherit&lt;/em&gt; specific features (eye color and shape, nose, smile, hair color and texture, skin tone) from both references, rather than averaging two faces — averaging is why first-generation "baby generators" produced children that looked like nobody.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part every "baby predictor" gets wrong
&lt;/h2&gt;

&lt;p&gt;Here's where I have to argue against my own product category. A baby's face &lt;strong&gt;cannot be predicted&lt;/strong&gt; — by my tool or any other. Appearance is polygenic: eye color alone involves several genes (the OCA2/HERC2 region does most of the work, not all of it), and each parent passes on one of two copies of every gene. The recombination is random. Even two full genomes don't compute a face.&lt;/p&gt;

&lt;p&gt;What a good generator can honestly claim is &lt;em&gt;fidelity to the parents&lt;/em&gt; — the imagined child should visibly inherit from both of you — not prediction of the child. So the copy on the site says exactly that: the result is a fictional, imagined person, and the page even explains &lt;a href="https://ourbaby.shop/what-will-my-baby-look-like/" rel="noopener noreferrer"&gt;why prediction is impossible&lt;/a&gt;. It turns out that honesty is also good SEO: it's the question people actually type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy structure, since your users' faces are your users
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Photos go encrypted to the model provider &lt;strong&gt;only&lt;/strong&gt; to generate the image&lt;/li&gt;
&lt;li&gt;No gallery, no public results wall — if you show other people's uploads, assume yours joins them&lt;/li&gt;
&lt;li&gt;The generated images live on the provider's temporary storage, not in my database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building anything that eats faces (headshots, restorations, family photos), I'd treat this as the baseline, not a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you before you build in this space
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bound your free tier with per-IP &lt;strong&gt;and&lt;/strong&gt; global daily caps on day one&lt;/li&gt;
&lt;li&gt;Label outputs as fictional — it's the difference between a keepsake and a scam&lt;/li&gt;
&lt;li&gt;Put the honest limit in the title of your main content page; being straight about what AI can't do outperforms promising predictions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see the result, the generator is at &lt;a href="https://ourbaby.shop/" rel="noopener noreferrer"&gt;ourbaby.shop&lt;/a&gt; — first preview free, no signup, and the &lt;a href="https://ourbaby.shop/ai-baby-generator-free/" rel="noopener noreferrer"&gt;free tier is documented in plain numbers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aiwebdevsideprojectshowdev</category>
    </item>
    <item>
      <title>AI Photo Restoration Keeps Changing Faces — Here's the Pipeline That Stops It</title>
      <dc:creator>tony huang</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:37:54 +0000</pubDate>
      <link>https://dev.to/tonytoolbox/ai-photo-restoration-keeps-changing-faces-heres-the-pipeline-that-stops-it-400n</link>
      <guid>https://dev.to/tonytoolbox/ai-photo-restoration-keeps-changing-faces-heres-the-pipeline-that-stops-it-400n</guid>
      <description>&lt;p&gt;If you've run old family photos through an AI restorer, you've probably hit the failure mode nobody advertises: it changes faces. Heavy restoration on a low-resolution scan doesn't recover a face — it re-draws a plausible one. You end up with a stranger wearing your grandmother's expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why faces drift
&lt;/h2&gt;

&lt;p&gt;Face restoration models are trained to produce &lt;em&gt;plausible&lt;/em&gt; faces. With a high-signal source, "plausible" and "correct" converge. Below a certain signal level — small scan, compression, damage sitting on the features — the model stops restoring and starts re-drawing. It's a fidelity-versus-strength trade-off baked into every tool of this class.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan at 600 dpi, in color&lt;/strong&gt; — color even for black-and-white originals, because the stain and tone information helps separate damage from image. A flat daylight photo of the print beats nothing, but a real scan beats everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair damage first, enhance faces second, upscale third&lt;/strong&gt; — separate passes, not one mega-pass. Scratches and fading act like added noise, and face restoration running on top of damage is where the drift starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bias toward fidelity, not strength&lt;/strong&gt; — the "beautiful" results that swap identity are usually high strength on low-res input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify at 100% zoom on eyes, smile, hairline&lt;/strong&gt; — if any of the three changed, reject the pass and re-run with less strength. That's the identity check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colorize last&lt;/strong&gt; — colorization is a guess about the past, not a record of it. Keep the black-and-white original as the archival master.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The hard ceiling
&lt;/h2&gt;

&lt;p&gt;Past roughly half of the facial detail missing, no model can recover identity — anything it produces is invention. That's manual-restoration territory. Below that threshold, the order of operations above is the difference between a sharper version of the same person and a stranger.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built with this
&lt;/h2&gt;

&lt;p&gt;I packaged this workflow into &lt;a href="https://aiphotoeditor.best/" rel="noopener noreferrer"&gt;AI Photo Editor&lt;/a&gt;: free local adjustments (crop, tone, contrast — nothing uploaded) plus a few free AI edits a day for enhance / restore / background removal. The local layer handles the prep steps without spending an AI credit, and the AI pass only ever sees the cleaned scan.&lt;/p&gt;

&lt;p&gt;Honest limits: it won't fix a photo beyond the reconstruction threshold above, and keeping the period look is the goal by design — if you want a "modernized" face, this is the wrong tool.&lt;/p&gt;

</description>
      <category>aiphotographywebdevtools</category>
    </item>
    <item>
      <title>Building browser-only tools that never upload your files (the local-first web stack)</title>
      <dc:creator>tony huang</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:09:06 +0000</pubDate>
      <link>https://dev.to/tonytoolbox/building-browser-only-tools-that-never-upload-your-files-the-local-first-web-stack-4hc8</link>
      <guid>https://dev.to/tonytoolbox/building-browser-only-tools-that-never-upload-your-files-the-local-first-web-stack-4hc8</guid>
      <description>&lt;p&gt;Most online tools quietly ship your files to a server: your photos, your invoices, your subtitle files, your client documents. There is a different way — build the whole tool client-side, so nothing ever leaves the device. I've been shipping a family of free tools built on this principle, and this post is the practical stack behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core pattern
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static site export.&lt;/strong&gt; The app is a Next.js static export on Cloudflare Pages — there is no backend to receive your files in the first place. Privacy is structural, not a policy promise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas API&lt;/strong&gt; for anything pixel-shaped: downscaling, palette quantisation, crops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly&lt;/strong&gt; for codecs that never shipped in browsers: HEIC decoding, WebP/AVIF encoding, GIF optimisation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebCodecs + seeking&lt;/strong&gt; for video work without ffmpeg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain text processing&lt;/strong&gt; where "format" just means text with rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Workers&lt;/strong&gt; so the main thread keeps breathing during heavy transforms.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image format conversion&lt;/strong&gt; — wasm builds of libheif/libwebp mean you can convert HEIC or WebP entirely locally, with batch support and no file-size ceiling. Live example: &lt;a href="https://imageconverter.site/" rel="noopener noreferrer"&gt;ImageConverter&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GIF compression&lt;/strong&gt; — scale, colour-reduce and drop frames in a worker. Live example: &lt;a href="https://gifcompressor.site/" rel="noopener noreferrer"&gt;GifCompressor&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video frames&lt;/strong&gt; — the browser can decode and seek video natively now: pull evenly spaced frames with timestamps, grab one exact frame, or scan a long clip for motion. No ffmpeg install, nothing uploaded. Live example: &lt;a href="https://videoframeextractor.site/" rel="noopener noreferrer"&gt;FrameExtractor&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel art conversion&lt;/strong&gt; — canvas downscale + palette quantisation turns photos into game-ready sprites. Live example: &lt;a href="https://pixelartconverter.site/" rel="noopener noreferrer"&gt;PixelArt&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtitle repair&lt;/strong&gt; — SRT/VTT/ASS are just text with rules, so parsing, resyncing and merging run instantly client-side. Live example: &lt;a href="https://subtoolbox.best/" rel="noopener noreferrer"&gt;SubToolbox&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculators&lt;/strong&gt; — pure JavaScript, zero I/O: aspect ratios (&lt;a href="https://aspectratio.site" rel="noopener noreferrer"&gt;https://aspectratio.site&lt;/a&gt;), marketplace seller fees (&lt;a href="https://sellerfees.org" rel="noopener noreferrer"&gt;https://sellerfees.org&lt;/a&gt;), construction materials (&lt;a href="https://buildcalcs.best" rel="noopener noreferrer"&gt;https://buildcalcs.best&lt;/a&gt;), electrical/HVAC tables (&lt;a href="https://tradecalcs.best" rel="noopener noreferrer"&gt;https://tradecalcs.best&lt;/a&gt;), even baby wake windows (&lt;a href="https://wakewindows.best" rel="noopener noreferrer"&gt;https://wakewindows.best&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest trade-offs
&lt;/h2&gt;

&lt;p&gt;Client-side is not free of constraints. Large files on low-RAM phones will hurt; you're bounded by the device instead of a server farm. Heavy AI inference is still mostly a server story (though small models in WASM are getting real). And some features — accounts, sync, sharing — reintroduce a backend by definition.&lt;/p&gt;

&lt;p&gt;But for a huge category of "give me a file, give me a different file back" problems, the browser is now enough. The user gets instant results, offline capability, and privacy that isn't just a checkbox. If you're building tools in 2026, it's worth asking first: &lt;em&gt;could this run entirely on the client?&lt;/em&gt; For us, the answer is usually yes — and it makes the product better, not just cheaper.&lt;/p&gt;




&lt;p&gt;All the tools linked above are free, require no sign-up, and run 100% in your browser. Feedback welcome in the comments.&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
  </channel>
</rss>
