cd /news/computer-vision/how-i-built-a-browser-only-face-rati… Β· home β€Ί topics β€Ί computer-vision β€Ί article
[ARTICLE Β· art-42561] src=dev.to β†— pub= topic=computer-vision verified=true sentiment=↑ positive

How I built a browser-only face-rating app with Next.js + MediaPipe (no upload, $0 per scan)

A developer built PSLRate, a browser-only face-rating app that computes attractiveness scores entirely client-side using MediaPipe's FaceLandmarker, with no image uploads or per-scan costs. The free tier analyzes facial geometry metrics like symmetry and proportions via WebGL, while a paid tier adds an LLM-generated personality reading. The architecture ensures infinite free usage by avoiding server-side AI vision APIs.

read4 min views1 publishedJun 28, 2026

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.

── more in #computer-vision 4 stories Β· sorted by recency
── more on @pslrate 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/how-i-built-a-browse…] indexed:0 read:4min 2026-06-28 Β· β€”