Description: On-device AI, WASM threads, target-size compression, inpainting and 3D LUTs in a single HTML file. 7 real lessons from buildi...
For further actions, you may consider blocking this person and/or reporting abuse
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
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.Got Python badge in this legendary post: dev.to/akhourianmolkumar/are-you-a...
Hope I'll get java script badge in this one... 🔥🔥🔥
👀
😂
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....
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 😂
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
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
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.