Most "rate my face" tools do one of two sketchy things: they upload your selfie to a server, or they bill an AI vision API on every single scan. I wanted neither. So when I built PSLRate, the rule was: the score has to be computed entirely in the browser β no upload, no per-scan cost.
Turns out the part of facial-attractiveness scoring that's actually reproducible β geometry β maps perfectly to that constraint. Here's how the whole thing runs client-side, and where I drew the line between "free and infinite" and "costs money."
When people say a face is "harmonious," a big chunk of it is measurable proportion: symmetry, the rule of facial thirds, the rule of fifths, eye tilt, facial width-to-height ratio. None of that needs a model on a server β it's just math on landmark coordinates.
So I split the product in two:
| Free tier | Paid tier | |
|---|---|---|
| Where it runs | Browser (your device) | |
| Server | ||
| What it does | Facial geometry β a 0β10 score + per-feature breakdown | An LLM writes a personality/"glow-up" reading |
| Cost per use | $0 | |
| An API call | ||
| Your photo | Never leaves the browser | |
| Sent once, on unlock |
The free tier being pure geometry is what makes it infinitely free. There's no API meter ticking. That single architectural decision is the whole trick.
MediaPipe Tasks Vision ships a FaceLandmarker
that runs on WebGL and returns 478 3D landmarks per face β entirely client-side. I self-host the model + wasm so there's no third-party CDN call (and it loads fine from regions where Google's CDN is flaky):
import { FaceLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
const landmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: { modelAssetPath: "/mediapipe/face_landmarker.task" },
runningMode: "IMAGE",
numFaces: 1,
});
const result = landmarker.detect(imageElement);
const pts = result.faceLandmarks[0]; // 478 points, each {x, y, z} normalized 0..1
That pts
array is everything I need. The image element is read straight from an <input type="file">
and a canvas β it never gets POSTed anywhere.
Every metric is "how close is this to an ideal, within a tolerance." So the core helper is just a clamped closeness function:
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
// 1.0 = exactly ideal, 0.0 = a full tolerance away or worse
const closeness = (deviation: number, tolerance: number) =>
clamp01(1 - Math.abs(deviation) / tolerance);
Symmetry β reflect the left-side landmarks across the face's vertical midline and measure how far they miss their right-side partners:
function symmetry(pts) {
const midX = (pts[33].x + pts[263].x) / 2; // midpoint between outer eye corners
let totalDev = 0;
for (const [l, r] of MIRROR_PAIRS) {
const reflectedLeftX = 2 * midX - pts[l].x;
totalDev += Math.abs(reflectedLeftX - pts[r].x);
}
return closeness(totalDev / MIRROR_PAIRS.length, SYMMETRY_TOL);
}
Facial thirds β forehead-to-brow, brow-to-nose-base, nose-base-to-chin should be roughly equal. Score the deviation from a perfect 1:1:1.
Facial fifths β the face should be about five eye-widths wide. Score the variance across those five segments.
Canthal tilt β the angle from inner to outer eye corner (a positive/upward tilt reads as "more attractive" in most studies). Computed per eye, then I penalize asymmetry between the two.
FWHR β facial width-to-height ratio, bizygomatic width over upper-face height.
Different metrics matter differently, so it's a weighted blend, then scaled to 0β10:
const dims = [
{ key: "symmetry", weight: 0.30 },
{ key: "thirds", weight: 0.25 },
{ key: "fifths", weight: 0.20 },
{ key: "tilt", weight: 0.15 },
{ key: "fwhr", weight: 0.10 },
];
const score10 =
dims.reduce((sum, d) => sum + metrics[d.key] * d.weight, 0) * 10;
Because it's deterministic, the same photo always yields the same score β which, for a "rate me" tool, matters a lot for trust. (Vision-LLM scores drift between runs; geometry doesn't.)
Numbers alone are a bit cold, so the paid tier sends the photo to a vision LLM once and gets back a written reading β archetype, personality notes, and a constructive, non-surgical "glow-up" direction. That's the only path where the image leaves the device, and it's gated behind a credit so the cost is covered. I keep the prompt firmly on the "fun + self-reflection" side and refuse anything that infers race/age or pushes surgery.
If you want to see the finished thing, it's live at ** PSLRate** β upload a selfie and the geometry score comes back in a couple of seconds, all in your browser. Happy to answer questions about the MediaPipe or scoring bits in the comments.