@mostajs/face 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FaceDetector.d.ts +27 -0
- package/dist/components/FaceDetector.js +177 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/package.json +29 -9
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface FaceDetectorProps {
|
|
2
|
+
/** Existing photo (base64) */
|
|
3
|
+
photo: string;
|
|
4
|
+
/** Callback when a photo is captured */
|
|
5
|
+
onCapture: (data: {
|
|
6
|
+
photo: string;
|
|
7
|
+
faceDescriptor: number[] | null;
|
|
8
|
+
}) => void;
|
|
9
|
+
/** Callback when photo is cleared */
|
|
10
|
+
onClear: () => void;
|
|
11
|
+
/** Verification mode: compare against existing descriptor */
|
|
12
|
+
verifyDescriptor?: number[];
|
|
13
|
+
/** Callback with verification result */
|
|
14
|
+
onVerifyResult?: (result: {
|
|
15
|
+
match: boolean;
|
|
16
|
+
distance: number;
|
|
17
|
+
} | null) => void;
|
|
18
|
+
/** Enable face detection (default: true) */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/** Match threshold (default: 0.6) */
|
|
21
|
+
threshold?: number;
|
|
22
|
+
/** Require face detected before capture (default: true) */
|
|
23
|
+
requireForCapture?: boolean;
|
|
24
|
+
/** Error callback instead of console.error */
|
|
25
|
+
onError?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export default function FaceDetector({ photo, onCapture, onClear, verifyDescriptor, onVerifyResult, enabled, threshold, requireForCapture, onError, }: FaceDetectorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mostajs/face — FaceDetector component
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
'use client';
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = FaceDetector;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const face_api_1 = require("../lib/face-api");
|
|
10
|
+
const face_matcher_1 = require("../lib/face-matcher");
|
|
11
|
+
const face_utils_1 = require("../lib/face-utils");
|
|
12
|
+
function FaceDetector({ photo, onCapture, onClear, verifyDescriptor, onVerifyResult, enabled = true, threshold = 0.6, requireForCapture = true, onError, }) {
|
|
13
|
+
const videoRef = (0, react_1.useRef)(null);
|
|
14
|
+
const canvasRef = (0, react_1.useRef)(null);
|
|
15
|
+
const overlayRef = (0, react_1.useRef)(null);
|
|
16
|
+
const animFrameRef = (0, react_1.useRef)(0);
|
|
17
|
+
const [streaming, setStreaming] = (0, react_1.useState)(false);
|
|
18
|
+
const [modelsReady, setModelsReady] = (0, react_1.useState)(false);
|
|
19
|
+
const [loadingModels, setLoadingModels] = (0, react_1.useState)(false);
|
|
20
|
+
const [faceDetected, setFaceDetected] = (0, react_1.useState)(false);
|
|
21
|
+
const [verifyResult, setVerifyResult] = (0, react_1.useState)(null);
|
|
22
|
+
const reportError = (0, react_1.useCallback)((msg) => {
|
|
23
|
+
if (onError)
|
|
24
|
+
onError(msg);
|
|
25
|
+
else
|
|
26
|
+
console.error('[FaceDetector]', msg);
|
|
27
|
+
}, [onError]);
|
|
28
|
+
// Load face-api models on mount
|
|
29
|
+
(0, react_1.useEffect)(() => {
|
|
30
|
+
if (!enabled)
|
|
31
|
+
return;
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
async function init() {
|
|
34
|
+
setLoadingModels(true);
|
|
35
|
+
try {
|
|
36
|
+
await (0, face_api_1.loadModels)();
|
|
37
|
+
if (!cancelled)
|
|
38
|
+
setModelsReady(true);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error('Face-api model loading error:', err);
|
|
42
|
+
if (!cancelled)
|
|
43
|
+
reportError('Impossible de charger les modeles de detection faciale');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (!cancelled)
|
|
47
|
+
setLoadingModels(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
init();
|
|
51
|
+
return () => { cancelled = true; };
|
|
52
|
+
}, [enabled, reportError]);
|
|
53
|
+
// Real-time detection loop
|
|
54
|
+
const detectLoop = (0, react_1.useCallback)(async () => {
|
|
55
|
+
if (!videoRef.current || !overlayRef.current)
|
|
56
|
+
return;
|
|
57
|
+
if (videoRef.current.paused || videoRef.current.ended)
|
|
58
|
+
return;
|
|
59
|
+
const video = videoRef.current;
|
|
60
|
+
const overlay = overlayRef.current;
|
|
61
|
+
const detection = await (0, face_api_1.detectFace)(video);
|
|
62
|
+
setFaceDetected(!!detection);
|
|
63
|
+
if (!videoRef.current || !overlayRef.current)
|
|
64
|
+
return;
|
|
65
|
+
(0, face_utils_1.drawDetection)(overlay, detection, video.videoWidth, video.videoHeight);
|
|
66
|
+
animFrameRef.current = requestAnimationFrame(detectLoop);
|
|
67
|
+
}, []);
|
|
68
|
+
const startCamera = (0, react_1.useCallback)(async () => {
|
|
69
|
+
setVerifyResult(null);
|
|
70
|
+
try {
|
|
71
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
72
|
+
video: { width: 320, height: 240, facingMode: 'user' },
|
|
73
|
+
});
|
|
74
|
+
if (videoRef.current) {
|
|
75
|
+
videoRef.current.srcObject = stream;
|
|
76
|
+
videoRef.current.play();
|
|
77
|
+
setStreaming(true);
|
|
78
|
+
if (enabled && modelsReady) {
|
|
79
|
+
videoRef.current.onloadeddata = () => {
|
|
80
|
+
animFrameRef.current = requestAnimationFrame(detectLoop);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
reportError("Impossible d'acceder a la camera");
|
|
87
|
+
}
|
|
88
|
+
}, [enabled, modelsReady, detectLoop, reportError]);
|
|
89
|
+
const stopCamera = (0, react_1.useCallback)(() => {
|
|
90
|
+
cancelAnimationFrame(animFrameRef.current);
|
|
91
|
+
if (videoRef.current?.srcObject) {
|
|
92
|
+
const tracks = videoRef.current.srcObject.getTracks();
|
|
93
|
+
tracks.forEach((track) => track.stop());
|
|
94
|
+
videoRef.current.srcObject = null;
|
|
95
|
+
}
|
|
96
|
+
setStreaming(false);
|
|
97
|
+
setFaceDetected(false);
|
|
98
|
+
}, []);
|
|
99
|
+
// Cleanup on unmount
|
|
100
|
+
(0, react_1.useEffect)(() => {
|
|
101
|
+
return () => {
|
|
102
|
+
cancelAnimationFrame(animFrameRef.current);
|
|
103
|
+
if (videoRef.current?.srcObject) {
|
|
104
|
+
const tracks = videoRef.current.srcObject.getTracks();
|
|
105
|
+
tracks.forEach((track) => track.stop());
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}, []);
|
|
109
|
+
const capturePhoto = (0, react_1.useCallback)(async () => {
|
|
110
|
+
if (!videoRef.current || !canvasRef.current)
|
|
111
|
+
return;
|
|
112
|
+
const canvas = canvasRef.current;
|
|
113
|
+
const video = videoRef.current;
|
|
114
|
+
canvas.width = video.videoWidth;
|
|
115
|
+
canvas.height = video.videoHeight;
|
|
116
|
+
const ctx = canvas.getContext('2d');
|
|
117
|
+
if (!ctx)
|
|
118
|
+
return;
|
|
119
|
+
ctx.drawImage(video, 0, 0);
|
|
120
|
+
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
|
121
|
+
let descriptor = null;
|
|
122
|
+
if (enabled && modelsReady) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = await (0, face_api_1.extractDescriptor)(canvas);
|
|
125
|
+
if (raw)
|
|
126
|
+
descriptor = Array.from(raw);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error('Descriptor extraction error:', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
stopCamera();
|
|
133
|
+
onCapture({ photo: dataUrl, faceDescriptor: descriptor });
|
|
134
|
+
}, [enabled, modelsReady, stopCamera, onCapture]);
|
|
135
|
+
const verifyFace = (0, react_1.useCallback)(async () => {
|
|
136
|
+
if (!videoRef.current || !verifyDescriptor)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
const raw = await (0, face_api_1.extractDescriptor)(videoRef.current);
|
|
140
|
+
if (!raw) {
|
|
141
|
+
setVerifyResult(null);
|
|
142
|
+
onVerifyResult?.(null);
|
|
143
|
+
reportError('Aucun visage detecte');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const distance = (0, face_matcher_1.compareFaces)(raw, verifyDescriptor);
|
|
147
|
+
const match = distance < threshold;
|
|
148
|
+
const result = { match, distance };
|
|
149
|
+
setVerifyResult(result);
|
|
150
|
+
onVerifyResult?.(result);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error('Face verification error:', err);
|
|
154
|
+
reportError('Erreur lors de la verification');
|
|
155
|
+
}
|
|
156
|
+
}, [verifyDescriptor, onVerifyResult, threshold, reportError]);
|
|
157
|
+
const captureDisabled = enabled && requireForCapture && !faceDetected;
|
|
158
|
+
// Basic webcam mode (face detection disabled)
|
|
159
|
+
if (!enabled) {
|
|
160
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '1rem' }, children: [photo ? ((0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative' }, children: [(0, jsx_runtime_1.jsx)("img", { src: photo, alt: "Photo", style: { width: '100%', borderRadius: '0.5rem' } }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onClear, style: { position: 'absolute', top: 8, right: 8, background: '#ef4444', color: 'white', border: 'none', borderRadius: '50%', width: 24, height: 24, cursor: 'pointer', fontSize: 14, lineHeight: 1 }, children: "x" })] })) : ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)("video", { ref: videoRef, style: { width: '100%', borderRadius: '0.5rem', display: streaming ? 'block' : 'none' }, autoPlay: true, playsInline: true, muted: true }), streaming ? ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: capturePhoto, style: { flex: 1, padding: '0.5rem', cursor: 'pointer' }, children: "Capturer" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: stopCamera, style: { padding: '0.5rem', cursor: 'pointer' }, children: "x" })] })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: startCamera, style: { width: '100%', padding: '0.5rem', cursor: 'pointer' }, children: "Prendre photo" }))] })), (0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, style: { display: 'none' } })] }));
|
|
161
|
+
}
|
|
162
|
+
// Full face detection mode
|
|
163
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '1rem' }, children: [photo ? ((0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative' }, children: [(0, jsx_runtime_1.jsx)("img", { src: photo, alt: "Photo", style: { width: '100%', borderRadius: '0.5rem' } }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onClear, style: { position: 'absolute', top: 8, right: 8, background: '#ef4444', color: 'white', border: 'none', borderRadius: '50%', width: 24, height: 24, cursor: 'pointer', fontSize: 14, lineHeight: 1 }, children: "x" })] })) : ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative', display: streaming ? 'block' : 'none' }, children: [(0, jsx_runtime_1.jsx)("video", { ref: videoRef, style: { width: '100%', borderRadius: '0.5rem' }, autoPlay: true, playsInline: true, muted: true }), (0, jsx_runtime_1.jsx)("canvas", { ref: overlayRef, style: { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' } }), (0, jsx_runtime_1.jsx)("div", { style: {
|
|
164
|
+
position: 'absolute', bottom: 8, left: 8,
|
|
165
|
+
padding: '2px 8px', borderRadius: 4, fontSize: 12, fontWeight: 500,
|
|
166
|
+
background: faceDetected ? 'rgba(34,197,94,0.9)' : 'rgba(239,68,68,0.9)',
|
|
167
|
+
color: 'white',
|
|
168
|
+
}, children: faceDetected ? 'Visage detecte' : 'Aucun visage' })] }), streaming ? ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: '0.5rem' }, children: [verifyDescriptor ? ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: verifyFace, disabled: !faceDetected, style: { flex: 1, padding: '0.5rem', cursor: faceDetected ? 'pointer' : 'not-allowed', opacity: faceDetected ? 1 : 0.5 }, children: "Verifier visage" })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: capturePhoto, disabled: captureDisabled, style: { flex: 1, padding: '0.5rem', cursor: captureDisabled ? 'not-allowed' : 'pointer', opacity: captureDisabled ? 0.5 : 1 }, children: captureDisabled ? 'Cadrez votre visage...' : 'Capturer' })), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: stopCamera, style: { padding: '0.5rem', cursor: 'pointer' }, children: "x" })] })) : ((0, jsx_runtime_1.jsx)("div", { children: loadingModels ? ((0, jsx_runtime_1.jsx)("button", { type: "button", disabled: true, style: { width: '100%', padding: '0.5rem', opacity: 0.5 }, children: "Chargement detection faciale..." })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: startCamera, style: { width: '100%', padding: '0.5rem', cursor: 'pointer' }, children: verifyDescriptor ? 'Verifier visage' : 'Prendre photo' })) }))] })), verifyResult && ((0, jsx_runtime_1.jsx)("div", { style: {
|
|
169
|
+
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
|
170
|
+
padding: '0.75rem', borderRadius: '0.5rem', fontSize: 14, fontWeight: 500,
|
|
171
|
+
background: verifyResult.match ? '#f0fdf4' : '#fef2f2',
|
|
172
|
+
color: verifyResult.match ? '#15803d' : '#b91c1c',
|
|
173
|
+
border: `1px solid ${verifyResult.match ? '#bbf7d0' : '#fecaca'}`,
|
|
174
|
+
}, children: verifyResult.match
|
|
175
|
+
? `Visage verifie (confiance: ${Math.round((1 - verifyResult.distance) * 100)}%)`
|
|
176
|
+
: `Visage non reconnu (distance: ${verifyResult.distance.toFixed(2)})` })), (0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, style: { display: 'none' } })] }));
|
|
177
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { useCamera } from './hooks/useCamera';
|
|
|
5
5
|
export { useFaceDetection } from './hooks/useFaceDetection';
|
|
6
6
|
export { createRecognizeHandler } from './api/recognize.route';
|
|
7
7
|
export { createDetectHandler } from './api/detect.route';
|
|
8
|
+
export { default as FaceDetector } from './components/FaceDetector';
|
|
9
|
+
export type { FaceDetectorProps } from './components/FaceDetector';
|
|
8
10
|
export type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor } from './types';
|
|
9
11
|
export type { FaceCandidate, RecognizeHandlerConfig } from './api/recognize.route';
|
|
10
12
|
export type { DetectHandlerConfig } from './api/detect.route';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// @mosta/face — Barrel exports
|
|
3
3
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
4
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.createDetectHandler = exports.createRecognizeHandler = exports.useFaceDetection = exports.useCamera = exports.drawDetection = exports.isValidDescriptor = exports.arrayToDescriptor = exports.descriptorToArray = exports.findAllMatches = exports.findMatch = exports.compareFaces = exports.extractDescriptor = exports.detectAllFaces = exports.detectFace = exports.isLoaded = exports.loadModels = void 0;
|
|
8
|
+
exports.FaceDetector = exports.createDetectHandler = exports.createRecognizeHandler = exports.useFaceDetection = exports.useCamera = exports.drawDetection = exports.isValidDescriptor = exports.arrayToDescriptor = exports.descriptorToArray = exports.findAllMatches = exports.findMatch = exports.compareFaces = exports.extractDescriptor = exports.detectAllFaces = exports.detectFace = exports.isLoaded = exports.loadModels = void 0;
|
|
6
9
|
// Core face-api service
|
|
7
10
|
var face_api_1 = require("./lib/face-api");
|
|
8
11
|
Object.defineProperty(exports, "loadModels", { enumerable: true, get: function () { return face_api_1.loadModels; } });
|
|
@@ -31,3 +34,6 @@ var recognize_route_1 = require("./api/recognize.route");
|
|
|
31
34
|
Object.defineProperty(exports, "createRecognizeHandler", { enumerable: true, get: function () { return recognize_route_1.createRecognizeHandler; } });
|
|
32
35
|
var detect_route_1 = require("./api/detect.route");
|
|
33
36
|
Object.defineProperty(exports, "createDetectHandler", { enumerable: true, get: function () { return detect_route_1.createDetectHandler; } });
|
|
37
|
+
// Components
|
|
38
|
+
var FaceDetector_1 = require("./components/FaceDetector");
|
|
39
|
+
Object.defineProperty(exports, "FaceDetector", { enumerable: true, get: function () { return __importDefault(FaceDetector_1).default; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/face",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Reusable face recognition module — detection, descriptor extraction, 1:N matching",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,15 +25,35 @@
|
|
|
25
25
|
"require": "./dist/hooks/useFaceDetection.js",
|
|
26
26
|
"default": "./dist/hooks/useFaceDetection.js"
|
|
27
27
|
},
|
|
28
|
-
"./
|
|
29
|
-
"types": "./dist/
|
|
30
|
-
"import": "./dist/
|
|
31
|
-
"default": "./dist/
|
|
28
|
+
"./components/FaceDetector": {
|
|
29
|
+
"types": "./dist/components/FaceDetector.d.ts",
|
|
30
|
+
"import": "./dist/components/FaceDetector.js",
|
|
31
|
+
"default": "./dist/components/FaceDetector.js"
|
|
32
32
|
},
|
|
33
|
-
"./api
|
|
34
|
-
"types": "./dist/api
|
|
35
|
-
"import": "./dist/api
|
|
36
|
-
"default": "./dist/api
|
|
33
|
+
"./lib/face-api": {
|
|
34
|
+
"types": "./dist/lib/face-api.d.ts",
|
|
35
|
+
"import": "./dist/lib/face-api.js",
|
|
36
|
+
"default": "./dist/lib/face-api.js"
|
|
37
|
+
},
|
|
38
|
+
"./lib/face-matcher": {
|
|
39
|
+
"types": "./dist/lib/face-matcher.d.ts",
|
|
40
|
+
"import": "./dist/lib/face-matcher.js",
|
|
41
|
+
"default": "./dist/lib/face-matcher.js"
|
|
42
|
+
},
|
|
43
|
+
"./lib/face-utils": {
|
|
44
|
+
"types": "./dist/lib/face-utils.d.ts",
|
|
45
|
+
"import": "./dist/lib/face-utils.js",
|
|
46
|
+
"default": "./dist/lib/face-utils.js"
|
|
47
|
+
},
|
|
48
|
+
"./api/recognize.route": {
|
|
49
|
+
"types": "./dist/api/recognize.route.d.ts",
|
|
50
|
+
"import": "./dist/api/recognize.route.js",
|
|
51
|
+
"default": "./dist/api/recognize.route.js"
|
|
52
|
+
},
|
|
53
|
+
"./api/detect.route": {
|
|
54
|
+
"types": "./dist/api/detect.route.d.ts",
|
|
55
|
+
"import": "./dist/api/detect.route.js",
|
|
56
|
+
"default": "./dist/api/detect.route.js"
|
|
37
57
|
},
|
|
38
58
|
"./types": {
|
|
39
59
|
"types": "./dist/types/index.d.ts",
|