@mostajs/face 1.1.0 → 1.3.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.
@@ -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,9 @@ 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 type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor } from './types';
8
+ export { default as FaceDetector } from './components/FaceDetector';
9
+ export type { FaceDetectorProps } from './components/FaceDetector';
10
+ export type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor, FaceSettings } from './types';
11
+ export { DEFAULT_FACE_SETTINGS } from './types';
9
12
  export type { FaceCandidate, RecognizeHandlerConfig } from './api/recognize.route';
10
13
  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.DEFAULT_FACE_SETTINGS = 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,8 @@ 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; } });
40
+ var types_1 = require("./types");
41
+ Object.defineProperty(exports, "DEFAULT_FACE_SETTINGS", { enumerable: true, get: function () { return types_1.DEFAULT_FACE_SETTINGS; } });
@@ -25,3 +25,14 @@ export interface FaceMatchResult<T> {
25
25
  distance: number;
26
26
  }
27
27
  export type FaceDescriptor = Float32Array | number[];
28
+ export interface FaceSettings {
29
+ /** Enable face detection/recognition (default: true) */
30
+ faceRecognitionEnabled: boolean;
31
+ /** Match distance threshold — lower = stricter (default: 0.6) */
32
+ faceRecognitionThreshold: number;
33
+ /** Require face detected before photo capture (default: true) */
34
+ faceRequireForCapture: boolean;
35
+ /** Auto-verify face after scan (default: false) */
36
+ faceAutoVerify: boolean;
37
+ }
38
+ export declare const DEFAULT_FACE_SETTINGS: FaceSettings;
@@ -2,3 +2,10 @@
2
2
  // @mosta/face — Types
3
3
  // Author: Dr Hamid MADANI drmdh@msn.com
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.DEFAULT_FACE_SETTINGS = void 0;
6
+ exports.DEFAULT_FACE_SETTINGS = {
7
+ faceRecognitionEnabled: true,
8
+ faceRecognitionThreshold: 0.6,
9
+ faceRequireForCapture: true,
10
+ faceAutoVerify: false,
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/face",
3
- "version": "1.1.0",
3
+ "version": "1.3.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
- "./lib/*": {
29
- "types": "./dist/lib/*.d.ts",
30
- "import": "./dist/lib/*.js",
31
- "default": "./dist/lib/*.js"
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/*.d.ts",
35
- "import": "./dist/api/*.js",
36
- "default": "./dist/api/*.js"
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",