작동 과정을 보여주는 AI 얼굴 평가 앱 제작: 브라우저의 MediaPipe와 서버의 LLM 활용
요약
사용자가 자신의 얼굴 매력도를 객관적으로 평가받을 수 있는 앱 [PSL Rating]을 개발했습니다. 이 도구는 브라우저의 MediaPipe를 이용해 랜드마크 기반 측정값을 먼저 계산하고, 이를 LLM(Gemini 등)에 전달하여 구조화된 JSON 형태로 평가 보고서를 받습니다. 특히 프롬프트 엔지니어링과 데이터 검증(Zod)을 통해 신뢰도를 높였습니다.
핵심 포인트
- MediaPipe를 활용해 브라우저에서 얼굴 랜드마크 측정값을 계산합니다.
- 측정값(객관적 근거)을 LLM에 전달하여 평가 보고서를 받습니다.
- 프롬프트와 구조화된 JSON 출력을 통해 평가의 신뢰도를 높였습니다.
- 안와 사시 각도 등 복잡한 지표는 보정을 위한 기준선 사용이 중요합니다.
많은 젊은이들이 자신이 얼마나 매력적인지 알고 싶어 하기 때문에 Gemini나 ChatGPT에게 "내 얼굴을 1점에서 10점까지 평가해 줘"라고 요청합니다. 두 번 물어보면 종종 다른 숫자를 받게 되며, 어떤 답변도 그 점수가 어떻게 도출되었는지 보여주지 않습니다. 그래서 저는 사용자가 직접 확인할 수 있는 근거와 함께 점수를 제공하는 보다 체계적인 평가 도구를 만들었습니다.
그 결과물이 바로 작은 얼굴 분석 앱인 PSL Rating입니다. 셀카 한 장을 업로드하면 PSL 척도에 따른 등급, 1~10점의 점수, 그리고 사진 위에 그려진 측정선들을 얻게 됩니다.
먼저 측정하고, 나중에 판단하기
측정은 브라우저에서 실행됩니다. 저는 MediaPipe의 Face Landmarker (@mediapipe/tasks-vision)를 사용하는데, 이는 홍채 중심을 포함하여 얼굴당 478개의 랜드마크(landmark)를 반환합니다. 이 지점들로부터 클라이언트가 얼굴 삼분할, 눈 간격, 안와 사시 각도(canthal tilt), 턱 너비 및 대칭성을 계산한 다음, 사진 위에 캔버스에 선을 그립니다. 이 과정 중 어느 것도 서버가 필요하지 않으며 결정론적입니다. 즉, 같은 사진은 항상 동일한 선을 제공합니다.

평가는 신중하게 조정된 프롬프트가 적용된 LLM에서 나옵니다. 사진, 측정값 텍스트 요약, 그리고 고정된 평가 기준표(rubric)가 비전 모델(Gemini, 서드파티 API를 통해)로 전송됩니다. 이 모델은 구조화된 JSON을 반환하며, 저는 이 데이터가 UI에 도달하기 전에 Zod로 검증합니다. 만약 필드가 누락되었거나 범위를 벗어난 경우, 전체 보고서를 무효화하는 대신 해당 섹션만 제외됩니다.
프롬프트의 중요성은 매우 큽니다. 모델이 이미지 옆에서 "눈 간격 비율 1.04, 안와 사시 각도 +4.1°"를 볼 때, 그 코멘트는 오버레이가 실제로 보여주는 내용에 훨씬 더 가깝게 유지됩니다.
제가 거의 배포할 뻔했던 안와 사시 각도 버그
안와 사시 각도는 얼굴 평가 논의에서 끊임없이 언급되는 주제입니다. 이는 눈 안쪽 구석부터 바깥쪽 구석까지의 선이 이루는 각도를 말합니다. 이를 계산하는 가장 직접적인 방법은 다음과 같습니다:
- 눈을 수평으로 맞추어 얼굴을 곧게 합니다.
- 각 눈의 각도를 측정합니다.
하지만 그건 틀립니다. 만약 눈 라인을 사용해 얼굴을 수평으로 만들면, 측정하려는 정확한 기울임(tilt) 자체를 회전시켜 버리고, 모든 사람이 0° 근처로 나오게 됩니다.
이 문제의 해결책은 눈을 통과하지 않는 선을 이용해 머리 롤(head roll)을 보정하는 것입니다. 저는 이마 상단(랜드마크 10)에서 턱(랜드마크 152)까지 이어지는 얼굴 중앙선(facial midline)을 사용합니다:
// 얼굴 자체의 롤: 중앙선(이마 -> 턱)이 수직에서 얼마나 기울어졌는지.
// 여기서 눈 라인을 사용하면 실제 안와 기울임(canthal tilt) 값을 상쇄시킵니다.
export function rollRad(px: Pt[]): number {
...
주의해야 할 두 가지 사항이 더 있습니다:
- 각도 측정 전에 정규화된 좌표를 픽셀로 변환하세요. MediaPipe는 x와 y 값을 각각 너비와 높이에 맞춰 0–1 범위로 반환합니다. 정사각형이 아닌 이미지에서 정규화된 좌표로 계산된 각도는 왜곡됩니다.
- 코너 랜드마크(Corner landmarks)는 노이즈가 심합니다. 프레임 간에 픽셀 단위로 떨립니다. 약 ±1° 정도의 작은 이미지에서는 UI가 값을 반올림하여, 0.6°와 같은 작은 값들을
실시간 카메라의 경우, 먼저 자동 캡처 카운트다운 기능을 만들었습니다. 제대로 작동하지 않았는데, 빌어먹을 카운트다운이 계속 재시작되는 바람에 말이죠. 이제는 사용자가 직접 누르는 큰 타원형 가이드와 셔터 버튼으로 바꿨습니다.
아키텍처 결정으로서의 프라이버시
얼굴 사진은 민감하므로, 디자인 원칙은 이를 보관하지 않는 것입니다:
- 사진은 평가 요청을 위해 전송되며 저장소에 절대 기록되지 않습니다. 데이터베이스에는 보고서의 텍스트와 숫자만 유지됩니다.
- 전체 보고서의 AI 스타일 미리보기(예: 다른 헤어스타일을 한 같은 얼굴, 이미지당 한 가지 변화)는 IndexedDB를 통해 사용자 브라우저 자체에 30일 동안 보관되며 로그아웃 시 삭제됩니다.
- 18세 이상만 가능합니다. 모든 사진은 AI 연령 확인을 거치며, 생성된 미리보기는 표시되기 전에 검열 과정을 거칩니다. 물론 모델이 가끔 틀릴 때도 있습니다. 사촌의 사진으로 테스트했을 때, 18세 미만이라고 판정했습니다. 그녀는 별로 화내지 않는 것 같았습니다.

트레이드오프는 사용자가 브라우저를 지우면 미리보기 이미지를 잃는다는 것입니다. 이러한 종류의 데이터에 대해서는 이것이 올바른 결정이라고 생각합니다.
스택
- Next.js 16을 Cloudflare Workers에 OpenNext로 배포
- 사용자, 크레딧 및 보고서를 위한 D1 (SQLite); 작은 캐시 값은 KV 사용
- Google 로그인, 계정당 무료 평가 1회 제공 후 유료 보고서
- 테스트를 위한 Vitest. 인메모리 스토어와
node:sqlite스토어를 사용하여 CI에서 D1 SQL 실행 가능
다르게 하고 싶은 것
저는 처음부터 픽셀 좌표와 기울기 보정(roll correction)을 포함하는 측정 코드를 작성하고, 합성된 랜드마크 세트(synthetic landmark sets)로 단위 테스트를 진행했을 것입니다. 초기 버그의 대부분은 좌표계 관련 버그였습니다.
프롬프트 튜닝 전에 사진 검사 기능을 먼저 구축했을 것입니다. 잘못된 입력이 프롬프트가 작동하는 모습을 실제보다 더 나쁘게 보이게 만들었기 때문입니다.
단어 선택에 더 신중했을 것입니다. 얼굴 점수는 누군가의 가치 판단으로 쉽게 읽힐 수 있습니다. 이 앱은 재미와 자기 계발을 위한 AI 추정치(AI estimates)라고 명시하며, 모든 제안에는 노력 수준(오늘, 주, 월, 구조적)이 태그되어 있어 사람들이 무엇을 변화시키는 것이 현실적인지 알 수 있게 합니다.
만약 MediaPipe나 비전 LLM(vision LLMs)을 사용하여 무언가를 구축한 경험이 있다면, 노이즈가 많은 랜드마크(noisy landmarks)나 일관성 없는 모델 출력(inconsistent model output)을 어떻게 처리했는지 듣고 싶습니다. 앱에 대한 피드백도 환영합니다: pslrating.me.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기