On-device face detection
for the web.

Glasses, head pose, blink — running fully local in the browser or Node.js via ONNX. Zero backend. Zero tracking.

Privacy-first
Sub-100ms inference
WASM · WebGPU · Node
import { useRef } from 'react';
import {
  useGlassesDetector,
  useHeadPoseDetector,
  useBlinkDetector,
} from '@framefind/react';

export function App() {
  const videoRef = useRef(null);

  const glasses  = useGlassesDetector({ videoRef });
  const headPose = useHeadPoseDetector({ videoRef });
  const blink    = useBlinkDetector({
    videoRef,
    onBlink: (ear) => console.log('blink!', ear),
    onFaceLost: () => console.log('face lost'),
  });

  return (
    <div>
      <video ref={videoRef} autoPlay playsInline muted />
      {glasses.loading && <p>Loading…</p>}
      {glasses.result  && <p>Glasses: {glasses.result.glasses ? 'yes' : 'no'}</p>}
      {headPose.result && <p>Yaw: {headPose.result.yaw.toFixed(1)}°</p>}
      {blink.result    && <p>Blinking: {blink.result.isBlinking ? 'yes' : 'no'}</p>}
    </div>
  );
}
$npm install @framefind/core @framefind/react
Modular Detectors

One platform, many signals.

All detectors live in @framefind/core with React hooks in @framefind/react. Models stream from CDN — zero bundle cost.

Available now

GlassesLive
@framefind/core

Detect eyewear presence with confidence score

Eye-region crop → 6.2MB ONNX classifier → smoothed probability
Head PoseLive
@framefind/core

Real-time yaw, pitch, and roll estimation

MediaPipe landmarks → solvePnP → ZYX Euler angles
BlinkLive
@framefind/core

Multi-signal blink detection with per-eye self-calibration

Blendshapes + EAR geometry + asymmetry → drop-rate gate → blink event
MaskLive
@framefind/core

Three-way face-mask classifier with on-device inference

Face-bbox crop → 112×112 ONNX classifier → softmax(with/without/incorrect)
GazeLive
@framefind/core

Iris-based gaze direction with 3×3 screen region mapping

Iris landmarks → eye-bbox ratio → head-pose compensation → gaze vector + region

On the roadmap

LivenessPhase 2
@framefind/core

Anti-spoof challenge for KYC and onboarding flows

Blink + head turn + smile challenge → texture analysis → liveness score
TalkingPhase 2
@framefind/core

Mouth-open detector for meeting UX and speaker indicators

Lip landmarks → Mouth Aspect Ratio → temporal gate → talking event
DrowsinessPhase 2
@framefind/core

Fatigue scoring from blink rate, yawns, and eye closure

Blink events + PERCLOS + yawn rate → temporal window → drowsiness score
AttentionPhase 2
@framefind/core

Engagement score from head pose and eye state

abs(yaw) < 20° && abs(pitch) < 15° && eyesOpen → 0–1 score
Pulse (rPPG)Phase 3
@framefind/core

Heart-rate estimation from facial micro-color changes

Forehead/cheek ROI → temporal RGB signal → POS / CHROM → BPM

How it works

Frame-to-result in milliseconds

Everything runs locally. No cloud round-trip. No raw video ever leaves the browser.

  1. Video Frame

    Camera · Image · Node

  2. MediaPipe

    468 facial landmarks

  3. ROI / Features

    Eye · Face bbox · EAR

  4. ONNX · Geometry

    WASM · WebGPU · solvePnP

  5. Result

    Class · Angles · Event

5

live detectors

0 bytes

data sent to server

<30ms

typical inference

Why FrameFind

The problem with existing APIs.

Cloud latency kills real-time UX

Sending 30fps over the network to cloud APIs causes severe lag. On-device inference delivers sub-100ms feedback with zero round-trips.

Face data shouldn't leave the device

Transmitting user face data to servers creates GDPR/CCPA overhead and erodes user trust. FrameFind guarantees data never leaves the browser.

Heavy runtimes stay out of your bundle

ONNX Runtime and MediaPipe are peer dependencies — loaded only when the detector needs them. Browser and Node builds ship as separate subpath exports, so your bundle stays minimal.

Models served from our own CDN

Weights and WASM runtimes are hosted on cdn.framefind.moraxh.dev (Cloudflare R2) — version-pinned, immutable cache, no jsdelivr/Google Storage dependency. ONNX Runtime stays a peer dep so your bundle stays lean.

Use cases

Built for real applications

Face signals unlock new interactions across industries — without sending data to a cloud.

EdTech

Attention monitoring

Detect when a student looks away from the screen to flag disengagement.

Telemedicine

Fatigue detection

Monitor blink rate and head drooping to alert fatigued healthcare workers.

Gaming

Gaze & head tracking

Use head pose as a no-controller input for hands-free interaction.

Accessibility

Eye tracking UI

Navigate interfaces by gaze for users with limited motor control.

Automotive

Drowsiness alerts

Detect microsleeps and distraction in driver-assistance systems.

Video Conferencing

Engagement scoring

Surface attention signals to meeting software without uploading video.

Quick start

Up and running in minutes.

Three steps from install to first detection result.

1

Install packages

Core + React
bash
npm install @framefind/core @framefind/react onnxruntime-web
Node.js
bash
npm install @framefind/core onnxruntime-node
2

Initialize a detector

tsx
const { videoRef, result, loading } = useGlassesDetector();

Or use the vanilla API: new GlassesDetector()

3

Read results in your render loop

tsx
if (result?.glasses) {
  console.log(`Glasses! ${(result.probability * 100).toFixed(1)}% confidence`);
}
import { useRef, useEffect } from 'react';
import { useGlassesDetector, useBlinkDetector } from '@framefind/react';

export default function App() {
  const videoRef = useRef<HTMLVideoElement>(null);

  const { result: glasses, loading } = useGlassesDetector({
    videoRef,
    threshold: 0.35,
  });

  const { result: blink } = useBlinkDetector({
    videoRef,
    onBlink: (ear)      => console.log('blink!', ear),
    onFaceLost: ()      => console.log('face lost'),
    onEARChange: (ear)  => console.log('ear:', ear),
  });

  useEffect(() => {
    let stream: MediaStream;
    navigator.mediaDevices
      .getUserMedia({ video: true })
      .then((s) => {
        stream = s;
        if (videoRef.current) videoRef.current.srcObject = s;
      });
    return () => stream?.getTracks().forEach((t) => t.stop());
  }, []);

  return (
    <div className="max-w-sm p-4">
      <video ref={videoRef} autoPlay playsInline muted className="w-full" />
      {loading && <p>Loading model…</p>}
      {glasses && (
        <p>Glasses: {glasses.glasses ? 'Yes' : 'No'} ({(glasses.probability * 100).toFixed(1)}%)</p>
      )}
      {blink && (
        <>
          <p>State: {blink.isBlinking ? 'closed' : 'open'}</p>
          <p>EAR: {blink.smoothedEar?.toFixed(3) ?? '—'}</p>
          <p>Baseline: {blink.baselineEar?.toFixed(3) ?? '—'}</p>
        </>
      )}
    </div>
  );
}