{"slug": "how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0", "title": "How I built a browser-only face-rating app with Next.js + MediaPipe (no upload, $0 per scan)", "summary": "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.", "body_md": "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](https://pslrate.com), the rule was: **the score has to be computed entirely in the browser — no upload, no per-scan cost.**\n\nTurns 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.\"\n\nWhen 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.\n\nSo I split the product in two:\n\n| Free tier | Paid tier | |\n|---|---|---|\n| Where it runs | Browser (your device) |\nServer |\n| What it does | Facial geometry → a 0–10 score + per-feature breakdown | An LLM writes a personality/\"glow-up\" reading |\n| Cost per use | $0 |\nAn API call |\n| Your photo | Never leaves the browser |\nSent once, on unlock |\n\nThe 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.\n\n[MediaPipe Tasks Vision](https://developers.google.com/mediapipe) ships a `FaceLandmarker`\n\nthat 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):\n\n``` js\nimport { FaceLandmarker, FilesetResolver } from \"@mediapipe/tasks-vision\";\n\nconst vision = await FilesetResolver.forVisionTasks(\"/mediapipe/wasm\");\nconst landmarker = await FaceLandmarker.createFromOptions(vision, {\n  baseOptions: { modelAssetPath: \"/mediapipe/face_landmarker.task\" },\n  runningMode: \"IMAGE\",\n  numFaces: 1,\n});\n\nconst result = landmarker.detect(imageElement);\nconst pts = result.faceLandmarks[0]; // 478 points, each {x, y, z} normalized 0..1\n```\n\nThat `pts`\n\narray is everything I need. The image element is read straight from an `<input type=\"file\">`\n\nand a canvas — it never gets POSTed anywhere.\n\nEvery metric is \"how close is this to an ideal, within a tolerance.\" So the core helper is just a clamped closeness function:\n\n``` js\nconst clamp01 = (x: number) => Math.max(0, Math.min(1, x));\n// 1.0 = exactly ideal, 0.0 = a full tolerance away or worse\nconst closeness = (deviation: number, tolerance: number) =>\n  clamp01(1 - Math.abs(deviation) / tolerance);\n```\n\n**Symmetry** — reflect the left-side landmarks across the face's vertical midline and measure how far they miss their right-side partners:\n\n``` js\nfunction symmetry(pts) {\n  const midX = (pts[33].x + pts[263].x) / 2; // midpoint between outer eye corners\n  let totalDev = 0;\n  for (const [l, r] of MIRROR_PAIRS) {\n    const reflectedLeftX = 2 * midX - pts[l].x;\n    totalDev += Math.abs(reflectedLeftX - pts[r].x);\n  }\n  return closeness(totalDev / MIRROR_PAIRS.length, SYMMETRY_TOL);\n}\n```\n\n**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.\n\n**Facial fifths** — the face should be about five eye-widths wide. Score the variance across those five segments.\n\n**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.\n\n**FWHR** — facial width-to-height ratio, bizygomatic width over upper-face height.\n\nDifferent metrics matter differently, so it's a weighted blend, then scaled to 0–10:\n\n``` js\nconst dims = [\n  { key: \"symmetry\", weight: 0.30 },\n  { key: \"thirds\",   weight: 0.25 },\n  { key: \"fifths\",   weight: 0.20 },\n  { key: \"tilt\",     weight: 0.15 },\n  { key: \"fwhr\",     weight: 0.10 },\n];\n\nconst score10 =\n  dims.reduce((sum, d) => sum + metrics[d.key] * d.weight, 0) * 10;\n```\n\nBecause 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.)\n\nNumbers 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.\n\nIf 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.", "url": "https://wpnews.pro/news/how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0", "canonical_source": "https://dev.to/alexlee_dev/how-i-built-a-browser-only-face-rating-app-with-nextjs-mediapipe-no-upload-0-per-scan-157m", "published_at": "2026-06-28 13:42:44+00:00", "updated_at": "2026-06-28 14:03:31.208899+00:00", "lang": "en", "topics": ["computer-vision", "developer-tools", "artificial-intelligence", "machine-learning", "generative-ai"], "entities": ["PSLRate", "MediaPipe", "Google", "WebGL", "FaceLandmarker", "Next.js"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0", "markdown": "https://wpnews.pro/news/how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0.md", "text": "https://wpnews.pro/news/how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0.txt", "jsonld": "https://wpnews.pro/news/how-i-built-a-browser-only-face-rating-app-with-next-js-mediapipe-no-upload-0.jsonld"}}