I'm Puneet Kumar, a 15-year-old full-stack developer. I love building innovative products, crafting beautiful web experiences, and turning ideas into reality.
I'm Puneet Kumar, a 15-year-old full-stack developer. I love building innovative products, crafting beautiful web experiences, and turning ideas into reality.
I'm Puneet Kumar, a 15-year-old full-stack developer. I love building innovative products, crafting beautiful web experiences, and turning ideas into reality.
Test 2 has a storage catch worth one more line. Cache API data is best-effort by default, so under storage pressure the browser can evict the whole origin, 176 MB model included, and the instant second run quietly turns into a fresh download. Calling await navigator.storage.persist() asks for persistent storage, and navigator.storage.estimate() tells you quota and usage before you start the 208 MB object remover. Checking the estimate first beats hitting a QuotaExceededError at cache.put() after every byte already arrived. Chrome grants persist() on its own heuristics, Firefox may ask the user, so treat it as a request.
It's an easy one to miss because nothing fails when it happens. One more piece: navigator.storage.persisted() tells you whether the grant actually happened, so the app can show it instead of guessing. Safari is worth a line too. Its storage rules can clear script-written data, Cache API included, for sites the user hasn't opened in a while, unless the app is added to the home screen. So for a 176 MB model I'd treat the cache as a speedup and keep the download path tested. Do you show a 'ready offline' state anywhere in the editor?
The SharedArrayBuffer / COOP-COEP trap is the one I would pin above any ONNX-in-browser checklist. Forcing numThreads = 1 on a static host is boring and correct; the silent phone hang when the "graceful" fallback never fires is the failure mode that actually burns users.
The inline Web Worker via Blob URL (Bonus) and the trilinear interpolation math for 3D LUTs (Test 6) are super clean! Doing binary search for target byte-size compression right in the browser is also such a practical trick. Great breakdown of real-world client-side constraints!
Great work, Akhouri! 🔥 Building all of this inside a single HTML file is impressive, especially the ONNX integration and the way you handle model downloads.
But the part that caught my attention wasn’t the AI. It was the privacy claim.
“Your photos never leave your device” is a powerful promise, but it also creates an interesting security question.
Even without a backend, the browser still executes JavaScript and downloads external resources. If any third-party script gets compromised, the fact that the application has no upload endpoint doesn’t necessarily prevent image data from being transmitted elsewhere.
Have you considered a strict Content Security Policy, dependency integrity verification, or making the entire application work offline after the initial model download?
I’d also be curious whether you’ve tested the application with all network access disabled after loading the models.
To me, that’s the difference between an application designed to process data locally and one whose privacy guarantees can actually be verified.
Thanks broooooo 🔥 honestly this is the best question anyone asked on this post as you are the most dangerous bone of the akhouri systems a important bone...
You're right. "No upload endpoint" is only a statement about code, not a guarantee. Any script running in the page can read the canvas. I went and checked my own setup after your comment, so here is the honest list:
the only fetch() in my own code is the model download, so nothing of mine sends images anywhere
but there is NO CSP yet and NO integrity hashes on the CDN scripts (ORT from cdnjs, Firebase from gstatic, emailjs from jsdelivr)
the ad banner runs in a sandboxed iframe, but right now ad.html is same-origin with the app, so that sandbox is weaker than it looks. thats the biggest hole, an ad script is exactly the "compromised third party" case you described
"Designed to process locally" vs "can actually be verified" is the line I'm stealing 🙏 thanks broooo
the USP is the code doesn't upload your image anywhere, any user can check it using dev tools network...
also bro I really loved you read the post but why only reading bruh, I would love if you use APIC WEB, lol you're expert so just bypass signup if you don't want to share it, but please use it...
Broooo, now THIS is the kind of conversation I love! 😂🔥
First of all, massive respect for actually going back and auditing your own implementation instead of just defending it. That’s exactly how good engineering happens.
And you found some interesting things!
One small distinction about the iframe: a sandboxed same-origin iframe isn’t automatically unsafe. The important details are which sandbox permissions you grant, whether scripts and same-origin access are enabled together, and what the embedded content can actually do.
For the CDN dependencies, I’d prioritize a restrictive CSP and SRI where applicable. But I’d be particularly careful with third-party scripts running in the main document, because once they execute there, they share the page’s privileges.
And here’s another interesting challenge: checking DevTools Network is useful, but it only proves what happened during that particular session. It doesn’t guarantee what a compromised dependency might do tomorrow.
As for APIC Web, haha, you got me! 😂 I spent more time reviewing the architecture than actually using the product.
I’ll give it a proper look. But you know me by now, bro. I’ll probably end up testing the security boundaries before editing a single photo. 😈
Keep building, and keep questioning your own assumptions. That’s where the interesting discoveries happen!
The COOP and COEP lesson is the one I would print out. GitHub Pages cannot send those headers, so you lose SharedArrayBuffer, and the fix that stopped phones hanging was forcing numThreads to 1. That is a hosting decision showing up as a performance bug weeks later.
Switching from a fixed timeout to a stall based one also makes sense when the models range from 5 MB to over 200 MB. A slow phone that is still making progress should not be treated the same as a dead connection.
I liked OnePassApps' suggestion of a DevTools network test as proof for the "never uploads" claim. It turns a privacy promise into something a skeptical reader can check in a minute, and Harry's note about bumping the cache name is the kind of detail that saves a support thread later.
At Ascendra Ventures we try to write claims like that as acceptance tests before a build starts. Did the LaMa to MI-GAN fallback change how you describe quality to users?
Thanks broo, and a hostiing decision showing up as a performance bug weeks later is exactly how it felt 😅....
Yesss, the fallback changed how I describe quality. The dropdown (Fast / Best / Ultra) is a request, not a promise, so the UI now reports what actually ran. If LaMa fails, the status says "Ultra engine unavailable — using standard AI", and the result label says it was MI-GAN, never "Ultra". If the AI model can't load at all, it says it did a quick fill and suggests retrying. Ultra (~208 MB) is only offered on desktop-class devices, so phones just keep MI-GAN......
i haven't written those as automated acceptance tests yet I like that approach, especially for the "never uploads" claim. A DevToolss network check as a repeatable test is on my list now....
Test 3's binary search is the right shape, but it has a quirk worth naming: you accept a mid only when blob.size <= targetBytes, and 7 iterations over [0.05, 0.98] leaves ~0.007 quality slack, so the returned blob can sit a few KB under the cap while a higher q would have fit — on a hard 200 KB limit I'd follow the search with one final probe at the accepted q plus a size guard, since re-encoding the same canvas at the same q can differ by a few bytes across engines and the server checks exact bytes. One question on Test 1: did you keep multi-threading behind the crossOriginIsolated check on hosts that do send COOP/COEP, and if so how much of the ONNX runtime's throughput did the single-thread path give up on desktop — wondering whether pinning numThreads=1 everywhere is costing enough that a proxy worker on a header-friendly host is worth the split code path.
I research AI tools, APIs & agents — comparing specs, pricing and real user reports — then publish honest rankings so you don't waste money. Research-first, never sponsored fluff.
Test 1 is the one that got me — "falls back gracefully" on desktop turning into a silent hang on a phone is exactly the kind of bug you only find by testing on the actual device. The stall-based timeout in test 2 is the fix I'd steal though; a fixed 30 seconds would kill every slow connection before the model even arrives. Was the phone hang the first one you hit, or did the PNG-going-black surprise land earlier?
Pingtou is spot on about the inline Web Worker via blob URL—that is an absolute lifesaver for keeping the UI thread from locking up during heavy processing.
Building complex AI interfaces (like the dashboard for GrantPro) taught me that the browser is often the real bottleneck, not the backend. I've run into similar memory limit crashes when handling large payloads.
Did you find WASM was consistently faster for the heavy image processing, or did the standard Canvas API hold up well enough for your use case? Great breakdown of the failures!
Thanks, Bogdan! Glad the blob-URL worker was useful. 🙌
To answer your question: they do different jobs, so it wasn’t really a speed contest. WASM (ONNX Runtime) is how the AI models run: background removal and the object remover. Plain Canvas handles everything else, like resizing, flattening alpha, the JPEG quality search and the colour grading. Canvas holds up fine there because the browser’s own encoders do the heavy lifting.
The catch is that on a static host like GitHub Pages I can’t send the COOP/COEP headers, so WASM runs single-threaded. That means it’s more “reliable on every phone” than “consistently fast”, and I haven’t benchmarked the two against each other, so I won’t pretend I have numbers. 😄
On memory crashes, three things helped me:
inpainting only runs on a crop around the brush instead of the full photo
ONNX’s memory arena and mem-pattern options are switched off
models are cached after the first download
What was the heavy payload in GrantPro? Curious whether you hit the limit in the browser or in the model step.
Rupato brand account. Product photo and video editing for ecommerce teams. Sharing practical ideas about image quality, review workflows and digital production. Operated by Liditech Ltd, UK.
Location
United Kingdom
Work
Product photo and video editing | Rupato, operated by Liditech Ltd
The transparency example in Test 4 raises a useful batch-export question. From the product-image side at Rupato, I would want the export screen to make two choices explicit: preserve transparency, or flatten onto a chosen background. A preview on white alone can hide the difference.
Does APIC let someone keep PNG transparency for one destination and export a white-background JPEG for another from the same edit? That would be a useful workflow to see alongside the target-file-size check.
Image compression and resizing tricks like this are super handy. When customers upload artwork files for custom boxes on our site at The Printing World, smooth browser resizing saves so many headaches before print production.
Test 1 hits the exact trap I've seen in Electron: "degrades gracefully on your dev machine" is a claim, not a promise — the equivalent of assuming your renderer behaves identically once COOP/COEP headers are missing or sandbox: true changes what APIs exist. Forcing the one configuration that behaves the same everywhere (ort numThreads=1 here, explicit webPreferences there) is the right call, and checking self.crossOriginIsolated before asking for threads is the same discipline as feature-detecting before touching privileged APIs. The "7 tests" framing is great because every lesson stays falsifiable instead of being a vibe.
Local processing in the browser solves a lot of privacy headaches, but memory management around canvas contexts can get tricky with larger files. Good breakdown on the edge cases.
the refresh-survival test hits hard. i once shipped a canvas editor that lost everything on reload and only found out from a user screenshot lol. solid checklist
Test 2 is the one I'd steal. Fixed timeouts are how we used to kill perfectly healthy downloads on slow lines, and stall-based abort is the right call — 176 MB on bad Wi-Fi is not a dead link.
One nit, IMO: the versioned cache name ('apic-models-v1') is the only thing standing between you and serving a stale model after a release. Bumping it by hand is exactly the kind of step that gets remembered for three releases and then forgotten, so it's probably worth deriving it from the model's own hash.
Test 1 is the one that got me — "falls back gracefully" on desktop turning into a silent hang on a phone is exactly the kind of bug that only shows up when you test on the real device. I also liked Test 2: switching from a fixed timeout to a stall-based one makes a lot of sense when model sizes range from 5 MB to 176 MB. Thanks for writing up the real failures instead of only the finished product.
We're a place where coders share, stay up-to-date and grow their careers.
Nice Post!!
Fastest reading 😆 🤣
nope...
1st --> comment on post
2nd --> read the post
lol
Okay this thread looks good...
yea... very clean
Lol I can't use gif I'm missing my signature meme
Test 2 has a storage catch worth one more line. Cache API data is best-effort by default, so under storage pressure the browser can evict the whole origin, 176 MB model included, and the instant second run quietly turns into a fresh download. Calling
await navigator.storage.persist()asks for persistent storage, andnavigator.storage.estimate()tells you quota and usage before you start the 208 MB object remover. Checking the estimate first beats hitting aQuotaExceededErroratcache.put()after every byte already arrived. Chrome grants persist() on its own heuristics, Firefox may ask the user, so treat it as a request.Yeah you're actually right I never thought of that it is a request
It's an easy one to miss because nothing fails when it happens. One more piece:
navigator.storage.persisted()tells you whether the grant actually happened, so the app can show it instead of guessing. Safari is worth a line too. Its storage rules can clear script-written data, Cache API included, for sites the user hasn't opened in a while, unless the app is added to the home screen. So for a 176 MB model I'd treat the cache as a speedup and keep the download path tested. Do you show a 'ready offline' state anywhere in the editor?The SharedArrayBuffer / COOP-COEP trap is the one I would pin above any ONNX-in-browser checklist. Forcing
numThreads = 1on a static host is boring and correct; the silent phone hang when the "graceful" fallback never fires is the failure mode that actually burns users.The inline Web Worker via
BlobURL (Bonus) and the trilinear interpolation math for 3D LUTs (Test 6) are super clean! Doing binary search for target byte-size compression right in the browser is also such a practical trick. Great breakdown of real-world client-side constraints!Great work, Akhouri! 🔥 Building all of this inside a single HTML file is impressive, especially the ONNX integration and the way you handle model downloads.
But the part that caught my attention wasn’t the AI. It was the privacy claim.
“Your photos never leave your device” is a powerful promise, but it also creates an interesting security question.
Even without a backend, the browser still executes JavaScript and downloads external resources. If any third-party script gets compromised, the fact that the application has no upload endpoint doesn’t necessarily prevent image data from being transmitted elsewhere.
Have you considered a strict Content Security Policy, dependency integrity verification, or making the entire application work offline after the initial model download?
I’d also be curious whether you’ve tested the application with all network access disabled after loading the models.
To me, that’s the difference between an application designed to process data locally and one whose privacy guarantees can actually be verified.
Really interesting project, bro. Keep building!
Thanks broooooo 🔥 honestly this is the best question anyone asked on this post as you are the most dangerous bone of the akhouri systems a important bone...
You're right. "No upload endpoint" is only a statement about code, not a guarantee. Any script running in the page can read the canvas. I went and checked my own setup after your comment, so here is the honest list:
Broooo, now THIS is the kind of conversation I love! 😂🔥
First of all, massive respect for actually going back and auditing your own implementation instead of just defending it. That’s exactly how good engineering happens.
And you found some interesting things!
One small distinction about the iframe: a sandboxed same-origin iframe isn’t automatically unsafe. The important details are which sandbox permissions you grant, whether scripts and same-origin access are enabled together, and what the embedded content can actually do.
For the CDN dependencies, I’d prioritize a restrictive CSP and SRI where applicable. But I’d be particularly careful with third-party scripts running in the main document, because once they execute there, they share the page’s privileges.
And here’s another interesting challenge: checking DevTools Network is useful, but it only proves what happened during that particular session. It doesn’t guarantee what a compromised dependency might do tomorrow.
As for APIC Web, haha, you got me! 😂 I spent more time reviewing the architecture than actually using the product.
I’ll give it a proper look. But you know me by now, bro. I’ll probably end up testing the security boundaries before editing a single photo. 😈
Keep building, and keep questioning your own assumptions. That’s where the interesting discoveries happen!
that monster emoji by someone with 20-25year+ experienced person bruhh I'm cooked 😂
The COOP and COEP lesson is the one I would print out. GitHub Pages cannot send those headers, so you lose SharedArrayBuffer, and the fix that stopped phones hanging was forcing numThreads to 1. That is a hosting decision showing up as a performance bug weeks later.
Switching from a fixed timeout to a stall based one also makes sense when the models range from 5 MB to over 200 MB. A slow phone that is still making progress should not be treated the same as a dead connection.
I liked OnePassApps' suggestion of a DevTools network test as proof for the "never uploads" claim. It turns a privacy promise into something a skeptical reader can check in a minute, and Harry's note about bumping the cache name is the kind of detail that saves a support thread later.
At Ascendra Ventures we try to write claims like that as acceptance tests before a build starts. Did the LaMa to MI-GAN fallback change how you describe quality to users?
Thanks broo, and a hostiing decision showing up as a performance bug weeks later is exactly how it felt 😅....
Yesss, the fallback changed how I describe quality. The dropdown (Fast / Best / Ultra) is a request, not a promise, so the UI now reports what actually ran. If LaMa fails, the status says "Ultra engine unavailable — using standard AI", and the result label says it was MI-GAN, never "Ultra". If the AI model can't load at all, it says it did a quick fill and suggests retrying. Ultra (~208 MB) is only offered on desktop-class devices, so phones just keep MI-GAN......
i haven't written those as automated acceptance tests yet I like that approach, especially for the "never uploads" claim. A DevToolss network check as a repeatable test is on my list now....
Got Python badge in this legendary post: dev.to/akhourianmolkumar/are-you-a...
Hope I'll get java script badge in this one... 🔥🔥🔥
👀
😂
Test 3's binary search is the right shape, but it has a quirk worth naming: you accept a mid only when blob.size <= targetBytes, and 7 iterations over [0.05, 0.98] leaves ~0.007 quality slack, so the returned blob can sit a few KB under the cap while a higher q would have fit — on a hard 200 KB limit I'd follow the search with one final probe at the accepted q plus a size guard, since re-encoding the same canvas at the same q can differ by a few bytes across engines and the server checks exact bytes. One question on Test 1: did you keep multi-threading behind the crossOriginIsolated check on hosts that do send COOP/COEP, and if so how much of the ONNX runtime's throughput did the single-thread path give up on desktop — wondering whether pinning numThreads=1 everywhere is costing enough that a proxy worker on a header-friendly host is worth the split code path.
Test 1 is the one that got me — "falls back gracefully" on desktop turning into a silent hang on a phone is exactly the kind of bug you only find by testing on the actual device. The stall-based timeout in test 2 is the fix I'd steal though; a fixed 30 seconds would kill every slow connection before the model even arrives. Was the phone hang the first one you hit, or did the PNG-going-black surprise land earlier?
I firstly hit actually phone hang png ones because it is more important if we compare to fixing png black issue
Pingtou is spot on about the inline Web Worker via blob URL—that is an absolute lifesaver for keeping the UI thread from locking up during heavy processing.
Building complex AI interfaces (like the dashboard for GrantPro) taught me that the browser is often the real bottleneck, not the backend. I've run into similar memory limit crashes when handling large payloads.
Did you find WASM was consistently faster for the heavy image processing, or did the standard Canvas API hold up well enough for your use case? Great breakdown of the failures!
Thanks, Bogdan! Glad the blob-URL worker was useful. 🙌
To answer your question: they do different jobs, so it wasn’t really a speed contest. WASM (ONNX Runtime) is how the AI models run: background removal and the object remover. Plain Canvas handles everything else, like resizing, flattening alpha, the JPEG quality search and the colour grading. Canvas holds up fine there because the browser’s own encoders do the heavy lifting.
The catch is that on a static host like GitHub Pages I can’t send the COOP/COEP headers, so WASM runs single-threaded. That means it’s more “reliable on every phone” than “consistently fast”, and I haven’t benchmarked the two against each other, so I won’t pretend I have numbers. 😄
On memory crashes, three things helped me:
inpainting only runs on a crop around the brush instead of the full photo
ONNX’s memory arena and mem-pattern options are switched off
models are cached after the first download
What was the heavy payload in GrantPro? Curious whether you hit the limit in the browser or in the model step.
The transparency example in Test 4 raises a useful batch-export question. From the product-image side at Rupato, I would want the export screen to make two choices explicit: preserve transparency, or flatten onto a chosen background. A preview on white alone can hide the difference.
Does APIC let someone keep PNG transparency for one destination and export a white-background JPEG for another from the same edit? That would be a useful workflow to see alongside the target-file-size check.
Image compression and resizing tricks like this are super handy. When customers upload artwork files for custom boxes on our site at The Printing World, smooth browser resizing saves so many headaches before print production.
Test 1 hits the exact trap I've seen in Electron: "degrades gracefully on your dev machine" is a claim, not a promise — the equivalent of assuming your renderer behaves identically once COOP/COEP headers are missing or sandbox: true changes what APIs exist. Forcing the one configuration that behaves the same everywhere (ort numThreads=1 here, explicit webPreferences there) is the right call, and checking self.crossOriginIsolated before asking for threads is the same discipline as feature-detecting before touching privileged APIs. The "7 tests" framing is great because every lesson stays falsifiable instead of being a vibe.
Local processing in the browser solves a lot of privacy headaches, but memory management around canvas contexts can get tricky with larger files. Good breakdown on the edge cases.
thanks
Interesting thought, thanks for sharing 👍🏼
and thanks for reading too bro 🫡🔥
the refresh-survival test hits hard. i once shipped a canvas editor that lost everything on reload and only found out from a user screenshot lol. solid checklist
Cool
Thanks 🙏🙏🙏🙂
Your Welcome
Intresting 🔥
🔥
Test 2 is the one I'd steal. Fixed timeouts are how we used to kill perfectly healthy downloads on slow lines, and stall-based abort is the right call — 176 MB on bad Wi-Fi is not a dead link.
One nit, IMO: the versioned cache name ('apic-models-v1') is the only thing standing between you and serving a stale model after a release. Bumping it by hand is exactly the kind of step that gets remembered for three releases and then forgotten, so it's probably worth deriving it from the model's own hash.
Test 1 is the one that got me — "falls back gracefully" on desktop turning into a silent hang on a phone is exactly the kind of bug that only shows up when you test on the real device. I also liked Test 2: switching from a fixed timeout to a stall-based one makes a lot of sense when model sizes range from 5 MB to 176 MB. Thanks for writing up the real failures instead of only the finished product.