@atgcentry/safe-capture-verifier
v1.0.1
Published
Identity verification camera component with TF FaceMesh, document ROI, quality gating and remote validation.
Maintainers
Readme
SafeCaptureVerifier React Component
SafeCaptureVerifier is a validation‑first identity capture widget for React applications.
It combines live camera capture, face landmarks + micro‑liveness (MediaPipe FaceMesh), and remote image analysis into a single, easy‑to‑embed component.
The component is designed for KYC / onboarding / identity flows where:
- A live face is required inside the frame
- The user holds an identity document in a guide box
- The app performs local quality checks (exposure, blur, edge density)
- A remote service performs SafeSearch + document detection
- Capture is only allowed when the face is detected and “armed”
Features
- ✔️ Live camera stream with device switching and optional torch toggle
- ✔️ MediaPipe FaceMesh via
@tensorflow-models/face-landmarks-detection- Face bounding box and landmarks overlay
- Sticky face gate: once a good face is seen, capture stays enabled while any face remains
- Simple micro‑liveness heuristic (movement + size change)
- ✔️ Static document guide (dotted white frame) for document positioning
- ✔️ Local image quality gates before any upload:
- Exposure
- Blur (Laplacian variance)
- Edge density
- ✔️ Remote validation via
endpointUrl+X_API_KEYheader - ✔️ SafeSearch parsing, face & document confidence, and telemetry hook
- ✔️ EN / FR built‑in translations, optional overrides, and future‑proof for additional locales
- ✔️ Responsive layout: mobile & desktop
- ✔️ Optional auto‑capture when face is stable & live
- ✔️ Optional consent checkbox before enabling capture
Installation
Make sure you have React and TypeScript set up.
# Core dependencies
yarn add react react-dom
# TensorFlow / FaceMesh stack
yarn add @tensorflow-models/face-landmarks-detection @mediapipe/face_mesh
# Icons (used by this component)
yarn add lucide-reactIf you use npm:
npm install \
react react-dom \
@tensorflow-models/face-landmarks-detection @mediapipe/face_mesh \
lucide-reactNote: The component itself is just a
.tsxfile; you can drop it into your project (e.g.src/components/SafeCaptureVerifier.tsx).
Basic Usage
import React, { useCallback } from "react";
import SafeCaptureVerifier, {
ValidationPayload,
TelemetryPayload,
} from "./SafeCaptureVerifier";
export const KycPage: React.FC = () => {
const handleValidated = useCallback(
(isSafe: boolean, analysis: ValidationPayload) => {
console.log("Validated?", isSafe, analysis);
// e.g. update local state or show summary to the user
},
[]
);
const handleConfirm = useCallback(
(file: File, analysis: ValidationPayload) => {
// This is called only when:
// - the image is marked as `safe`
// - there is a valid analysis payload
// The parent can then upload `file` to its own backend.
console.log("Confirmed snapshot:", file, analysis);
},
[]
);
const handleFailure = useCallback((err: unknown) => {
console.error("Validation error:", err);
}, []);
const handleTelemetry = useCallback((payload: TelemetryPayload) => {
console.debug("Telemetry:", payload);
}, []);
return (
<SafeCaptureVerifier
apiKey={import.meta.env.VITE_CENTRY_API_KEY}
endpointUrl="https://api.centry.uk/v1/ocr/api/analyze"
requestId="signup-kyc-12345"
locale="en"
minDocConfidence={89}
countdownSeconds={5}
autoCapture
onValidated={handleValidated}
onConfirm={handleConfirm}
onFailure={handleFailure}
onTelemetry={handleTelemetry}
brandImageUrl="/assets/brand-logo.svg"
/>
);
};At a minimum you must provide:
apiKey– the API key sent asX_API_KEYheader to your backend / analysis service.
If you don’t pass endpointUrl, it defaults to:
https://api.centry.uk/v1/ocr/api/analyze
Component API
SafeCaptureVerifier Props
export type SafeSearchLikelihood =
| "UNKNOWN"
| "VERY_UNLIKELY"
| "UNLIKELY"
| "POSSIBLE"
| "LIKELY"
| "VERY_LIKELY";
export interface SafeSearchAnnotation {
Adult: SafeSearchLikelihood;
Medical: SafeSearchLikelihood;
Racy: SafeSearchLikelihood;
Spoof: SafeSearchLikelihood;
Violence: SafeSearchLikelihood;
}
export interface ValidationPayload {
SafeSearch: string; // Raw SafeSearch string (parsed internally)
Faces?: unknown[]; // Face detections from backend (if any)
Labels: Record<string, number>; // Label -> score map
}
export interface TelemetryPayload {
requestId?: string | number;
faceDetected: boolean;
faceQualityPct: number; // 0..100
livenessScore: number;
localChecks: {
exposureOk: boolean;
blurOk: boolean;
edgesOk: boolean;
};
remote?: {
safe: boolean;
docConfidence: number;
safeSearch?: SafeSearchAnnotation | null;
};
}Main Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiKey | string | required | API key sent as X_API_KEY header to the validation endpoint. |
| endpointUrl | string | "https://api.centry.uk/v1/ocr/api/analyze" | Remote image analysis endpoint. Receives a FormData with image and optional title. |
| requestId | string \| number | undefined | Optional identifier attached to the validation request as title. Useful for correlating backend logs. |
| locale | "en" \| "fr" | "en" | Current UI language. |
| labels | Partial<LabelSchema> | undefined | Override or extend the built‑in EN/FR translations. |
| brandImageUrl | string | undefined | Optional logo shown above the analysis panel. |
| countdownSeconds | number | 10 | Countdown duration before the snapshot is taken once the user clicks the capture button. |
| minDocConfidence | number | 89 | Minimum "Identity document" label score (in %) required for the capture to be considered safe. |
| acceptedLikelihoods | SafeSearchLikelihood[] | ["VERY_UNLIKELY","UNLIKELY"] | Allowed SafeSearch likelihoods for all categories. Any category outside this set fails the validation. |
| autoCapture | boolean | false | If true, starts the countdown automatically when the face is stable and liveness is OK. |
| autoCaptureStableFrames | number | 10 | Number of stable frames required before auto‑capture triggers. |
| requireConsent | boolean | false | If true, shows a consent checkbox and keeps the capture button disabled until the user agrees. |
| onConsentChange | (consented: boolean) => void | undefined | Called whenever the consent checkbox changes. |
| enableTfFace | boolean | true | Enables MediaPipe FaceMesh for face detection / landmarks / liveness. |
| detectionFps | number | 8 | Target detection FPS for the FaceMesh loop. |
| livenessWindow | number | 12 | Number of frames in the liveness history window. |
| livenessThreshold | number | 0.015 | Threshold on movement + area change; above this, liveness is considered OK. |
| showOverlay | boolean | true | If true, draws the face bounding box and sparse landmarks on a canvas overlay. |
| enableTorch | boolean | true | If supported by the device, shows a torch toggle button. |
| docRoiInset | number | 0.12 | Creates a white dotted static guide box inset by this fraction from each border. Only visual – no document detection. |
Callback Props
| Prop | Signature | Description |
|------|-----------|-------------|
| onValidated | (isSafe: boolean, analysis: ValidationPayload) => void | Called after the remote validator responds, whether the snapshot is safe or not. |
| onConfirm | (file: File, analysis: ValidationPayload) => void | Called when the user submits the form and the capture is considered safe (meets SafeSearch, face, and document confidence criteria). |
| onFailure | (error: unknown) => void | Called when remote validation fails (network or server error). |
| onTelemetry | (payload: TelemetryPayload) => void | Optional analytics hook for logging local quality metrics, liveness score, and remote result. |
i18n and Label Overrides
The component ships with a LabelSchema for English (en) and French (fr) and merges your overrides on top.
import { LabelSchema } from "./SafeCaptureVerifier";
const customLabels: Partial<LabelSchema> = {
title: "Identity Check",
subtitle: "Please hold your ID and align your face in the frame.",
errors: {
// only override what you want
apiValidation: "Unable to validate at this time. Try again later.",
},
};<SafeCaptureVerifier
apiKey={apiKey}
locale="fr"
labels={customLabels}
/>Any missing field falls back to the internal defaults.
Consent‑Gated Capture
To require explicit consent before capture is possible:
<SafeCaptureVerifier
apiKey={apiKey}
requireConsent
onConsentChange={(consented) => {
console.log("User consented?", consented);
}}
/>Until the user checks “I understand and agree”, the capture button remains disabled.
Auto‑Capture Flow
You can enable auto‑capture so that the user does not need to click the camera button explicitly; the system will start the countdown once the face is stable and liveness is OK:
<SafeCaptureVerifier
apiKey={apiKey}
autoCapture
autoCaptureStableFrames={12} // number of stable frames (~1–2 seconds)
/>The component still shows the countdown overlay. When the countdown reaches 0, it takes a snapshot, runs local quality checks, and sends it to your backend.
Handling the Confirmed Snapshot
The component wraps everything in a <form>; it calls onConfirm only when the latest snapshot is considered safe:
const handleConfirm = (file: File, analysis: ValidationPayload) => {
const formData = new FormData();
formData.append("image", file);
formData.append("safeScore", String(analysis.Labels["Identity document"] ?? 0));
fetch("/api/kyc/upload", {
method: "POST",
body: formData,
})
.then((res) => res.json())
.then((res) => console.log("Uploaded to our backend:", res))
.catch((err) => console.error(err));
};
<SafeCaptureVerifier
apiKey={apiKey}
onConfirm={handleConfirm}
/>;Your backend is free to store the original file; the component itself never persists it.
Telemetry and Observability
onTelemetry is useful for dashboards, debugging or offline tuning. Example:
const handleTelemetry = (p: TelemetryPayload) => {
// Example: send to your logging / metrics service
console.debug("[SafeCapture] telemetry", p);
};
<SafeCaptureVerifier
apiKey={apiKey}
onTelemetry={handleTelemetry}
/>;You’ll get both local metrics (exposure, blur, edge density) and remote verdicts (SafeSearch, doc confidence) in a single object.
Styling & Layout
The component uses inline styles only, no external CSS. It is:
- Responsive by design – uses a simple breakpoint hook (
useBreakpoint) to switch between single‑column and two‑column layout. - Uses only neutral colors and clean visual hierarchy:
- Left: live camera with face overlay and dotted document guide
- Right: latest snapshot preview with retake button
- Below: analysis panel and optional error alert
If you prefer, you can wrap it in your own layout container or place it inside a modal / dialog component. The width scales with its parent node.
Notes & Best Practices
HTTPS & Permissions
Browsers require HTTPS for camera access in production. Make sure your app runs over HTTPS, or uselocalhostin development.Device Support
Torch support depends on the browser & device. The component fails gracefully if torch is not available.Backend Contract
The remote endpoint is expected to return a JSON payload with either:payload: ValidationPayload, orValidationPayloadat the top level.
The relevant parts are:
SafeSearch: a string like"Adult: VERY_UNLIKELY, Violence: UNLIKELY, ..."Faces: an array (non‑empty if faces detected)Labels["Identity document"]: a numeric 0–100 score
Liveness
Liveness is a lightweight heuristic, not a full anti‑spoof system. For high‑risk use cases, combine it with stronger server‑side checks.
License
This component is provided as part of a private project integration.
Adapt, extend, and integrate it into your own codebase according to your project’s license and compliance requirements.
