How it works
Give it a picture, get it back with the meme sunglasses on every face in it.




Group photo: the 2013 class of NASA astronauts, by Robert Markowitz for NASA. Public domain.
Faces are found with YuNet, a small ONNX detector run through OpenCV. If it finds nothing it looks again at the picture doubled in size, then with its contrast equalised. Each face then goes to dlib's 68-point shape predictor for the landmarks the glasses are placed from.
The line between the outer corners of the eyes gives the tilt of the head and the size of the frame; how far the nose sits off that line gives the turn, so the glasses narrow and taper on a face seen from the side. Every point found comes back in the result, as faces. It also finds dogs now and then; cats, never.
Compositing is done with Pillow in a background worker managed by RQ: detecting faces takes long enough that doing it inside the request would block everything else. The page is FastHTML + htmx, the REST API is FastAPI.
API
Documented at /api/docs. Processing happens in the background, so it takes two calls: one to submit the image, one to collect the result.
1Submit an image
POST /api/jobs with either a url or a base64 data URI, not both:
{
"url": "https://upload.wikimedia.org/wikipedia/commons/3/3d/Apollo_11_Crew.jpg"
}Copied {
"base64": "data:image/png;base64,..."
}Copied
Responds 202 with the job to poll:
{
"job_id": "9f2c...",
"state": "queued",
"status_url": "http://localhost:5000/api/jobs/9f2c..."
}Copied 2Collect the result
GET the status_url until state is finished or failed:
{
"job_id": "9f2c...",
"state": "finished",
"images": {
"view": ".../i/9f2c.../view.webp",
"thumb": ".../i/9f2c.../thumb.webp",
"before": ".../i/9f2c.../before.webp",
"animation": ".../i/9f2c.../animation.gif",
"full": ".../i/9f2c.../result.jpg"
},
"downloads": {"jpg": "...", "webp": "...", "gif": "..."},
"share_url": ".../s/9f2c...",
"expires_at": "2026-09-04T17:32:00Z",
"error": null,
"progress": 100,
"step": "Done",
"detection": "plain",
"faces": [{"box": [101, 420, 402, 197], "score": 0.95,
"points": {"left_eye": [249.7, 225.3], ...},
"landmarks": [[199.0, 208.0], ...]}]
}Copied
images are links, not bytes. view is what to show someone -- WebP, 1600px, a fortieth of the full-resolution file -- and full keeps the format the picture was submitted in. animation is the glasses dropping onto every face, as a looping GIF. downloads offers the same picture in whichever formats were written for it. Everything under /i/ stops existing at expires_at, and so does share_url.
state is one of queued, started, finished or failed. A failed job carries a readable error: an unreachable URL, an undecodable image, or no faces found. An unknown or expired job id gives a 404. The result is a JPEG when the input was a JPEG and a PNG otherwise.
Before version 2, the result came back inline as image: a base64 data URI, megabytes of it, in every poll. That field is gone.
Submissions are limited per client and refused with 429 (too many in a minute) or 503 (the queue is full); both carry a Retry-After header.
A queued job also carries ahead: how many jobs are in front of it, with 0 meaning it is next. It goes null once a worker picks the job up, which is when progress and step take over.
While a job runs, progress and step report the stage it has reached. They are checkpoints rather than measurements: neither the detector nor Pillow reports how far through it is.
Health
GET /api/health reports whether the broker is reachable. The status pill in the header reads it every 30 seconds.
{ "status": "ok", "redis": true }Copied License
MIT License. Created by Eric Magalhães.