@mostajs/face 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dr Hamid MADANI <drmdh@msn.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @mostajs/face
2
+
3
+ > Reusable face recognition module — detection, descriptor extraction, 1:N matching.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@mostajs/face.svg)](https://www.npmjs.com/package/@mostajs/face)
6
+ [![license](https://img.shields.io/npm/l/@mostajs/face.svg)](LICENSE)
7
+
8
+ Part of the [@mosta suite](https://mostajs.dev).
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @mostajs/face
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ### 1. Load models and detect faces
21
+
22
+ ```typescript
23
+ import { loadModels, detectFace, extractDescriptor } from '@mostajs/face'
24
+
25
+ await loadModels('/models') // path to face-api.js model files
26
+
27
+ const detection = await detectFace(imageElement)
28
+ const descriptor = await extractDescriptor(imageElement)
29
+ ```
30
+
31
+ ### 2. Compare faces
32
+
33
+ ```typescript
34
+ import { compareFaces, findMatch } from '@mostajs/face'
35
+
36
+ const distance = compareFaces(descriptor1, descriptor2)
37
+
38
+ const match = findMatch(unknownDescriptor, knownFaces, 0.6)
39
+ if (match) {
40
+ console.log('Matched:', match.label, 'distance:', match.distance)
41
+ }
42
+ ```
43
+
44
+ ### 3. React hooks
45
+
46
+ ```tsx
47
+ import { useCamera } from '@mostajs/face/hooks/useCamera'
48
+ import { useFaceDetection } from '@mostajs/face/hooks/useFaceDetection'
49
+
50
+ function FaceCapture() {
51
+ const { videoRef, start, stop } = useCamera()
52
+ const { detection, descriptor } = useFaceDetection(videoRef)
53
+ // ...
54
+ }
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ | Export | Description |
60
+ |--------|-------------|
61
+ | `loadModels(path)` | Load face-api.js models |
62
+ | `detectFace(input)` | Detect single face with landmarks |
63
+ | `detectAllFaces(input)` | Detect all faces |
64
+ | `extractDescriptor(input)` | Extract 128-dim face descriptor |
65
+ | `compareFaces(d1, d2)` | Euclidean distance between descriptors |
66
+ | `findMatch(descriptor, known, threshold)` | Find best match |
67
+ | `findAllMatches(descriptor, known, threshold)` | Find all matches |
68
+ | `useCamera()` | Camera management hook |
69
+ | `useFaceDetection()` | Continuous detection hook |
70
+
71
+ ## License
72
+
73
+ MIT — © 2025 Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,17 @@
1
+ export type CameraStatus = 'idle' | 'requesting' | 'active' | 'denied' | 'error';
2
+ interface UseCameraOptions {
3
+ facingMode?: 'user' | 'environment';
4
+ width?: number;
5
+ height?: number;
6
+ autoStart?: boolean;
7
+ }
8
+ export declare function useCamera(options?: UseCameraOptions): {
9
+ videoRef: import("react").RefObject<HTMLVideoElement | null>;
10
+ stream: MediaStream | null;
11
+ status: CameraStatus;
12
+ error: string | null;
13
+ start: () => Promise<void>;
14
+ stop: () => void;
15
+ switchCamera: () => Promise<void>;
16
+ };
17
+ export {};
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ // @mosta/face — useCamera hook
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ 'use client';
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useCamera = useCamera;
7
+ const react_1 = require("react");
8
+ function useCamera(options) {
9
+ const { facingMode = 'user', width = 640, height = 480, autoStart = false } = options || {};
10
+ const videoRef = (0, react_1.useRef)(null);
11
+ const [stream, setStream] = (0, react_1.useState)(null);
12
+ const [status, setStatus] = (0, react_1.useState)('idle');
13
+ const [error, setError] = (0, react_1.useState)(null);
14
+ const start = (0, react_1.useCallback)(async () => {
15
+ try {
16
+ setStatus('requesting');
17
+ setError(null);
18
+ const mediaStream = await navigator.mediaDevices.getUserMedia({
19
+ video: { facingMode, width: { ideal: width }, height: { ideal: height } },
20
+ });
21
+ if (videoRef.current) {
22
+ videoRef.current.srcObject = mediaStream;
23
+ await videoRef.current.play();
24
+ }
25
+ setStream(mediaStream);
26
+ setStatus('active');
27
+ }
28
+ catch (err) {
29
+ if (err.name === 'NotAllowedError') {
30
+ setStatus('denied');
31
+ setError('Camera permission denied');
32
+ }
33
+ else {
34
+ setStatus('error');
35
+ setError(err.message || 'Camera error');
36
+ }
37
+ }
38
+ }, [facingMode, width, height]);
39
+ const stop = (0, react_1.useCallback)(() => {
40
+ if (stream) {
41
+ stream.getTracks().forEach((t) => t.stop());
42
+ setStream(null);
43
+ }
44
+ if (videoRef.current) {
45
+ videoRef.current.srcObject = null;
46
+ }
47
+ setStatus('idle');
48
+ }, [stream]);
49
+ const switchCamera = (0, react_1.useCallback)(async () => {
50
+ stop();
51
+ // Toggle facing mode
52
+ const newMode = facingMode === 'user' ? 'environment' : 'user';
53
+ try {
54
+ const mediaStream = await navigator.mediaDevices.getUserMedia({
55
+ video: { facingMode: newMode, width: { ideal: width }, height: { ideal: height } },
56
+ });
57
+ if (videoRef.current) {
58
+ videoRef.current.srcObject = mediaStream;
59
+ await videoRef.current.play();
60
+ }
61
+ setStream(mediaStream);
62
+ setStatus('active');
63
+ }
64
+ catch (err) {
65
+ setStatus('error');
66
+ setError(err.message);
67
+ }
68
+ }, [stop, facingMode, width, height]);
69
+ (0, react_1.useEffect)(() => {
70
+ if (autoStart)
71
+ start();
72
+ return () => {
73
+ // Cleanup on unmount
74
+ stream?.getTracks().forEach((t) => t.stop());
75
+ };
76
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
77
+ return { videoRef, stream, status, error, start, stop, switchCamera };
78
+ }
@@ -0,0 +1,21 @@
1
+ import type { MostaFaceConfig } from '../types';
2
+ export type DetectionStatus = 'loading' | 'detecting' | 'detected' | 'noFace';
3
+ interface UseFaceDetectionOptions {
4
+ /** Interval between detections in ms (default: 300) */
5
+ interval?: number;
6
+ /** Auto-start detection (default: true) */
7
+ autoStart?: boolean;
8
+ /** Also extract descriptor on each frame (default: false) */
9
+ extractDescriptor?: boolean;
10
+ /** Face-api config */
11
+ config?: MostaFaceConfig;
12
+ }
13
+ export declare function useFaceDetection(videoRef: React.RefObject<HTMLVideoElement | null>, options?: UseFaceDetectionOptions): {
14
+ detection: any;
15
+ descriptor: Float32Array<ArrayBufferLike> | null;
16
+ status: DetectionStatus;
17
+ modelsLoaded: boolean;
18
+ stop: () => void;
19
+ start: () => Promise<void>;
20
+ };
21
+ export {};
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // @mosta/face — useFaceDetection hook
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ 'use client';
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.useFaceDetection = useFaceDetection;
40
+ const react_1 = require("react");
41
+ function useFaceDetection(videoRef, options) {
42
+ const { interval = 300, autoStart = true, extractDescriptor: doExtract = false, config } = options || {};
43
+ const [detection, setDetection] = (0, react_1.useState)(null);
44
+ const [descriptor, setDescriptor] = (0, react_1.useState)(null);
45
+ const [status, setStatus] = (0, react_1.useState)('loading');
46
+ const [modelsLoaded, setModelsLoaded] = (0, react_1.useState)(false);
47
+ const running = (0, react_1.useRef)(false);
48
+ const timerRef = (0, react_1.useRef)(null);
49
+ const loadAndStart = (0, react_1.useCallback)(async () => {
50
+ const faceApi = await Promise.resolve().then(() => __importStar(require('../lib/face-api')));
51
+ await faceApi.loadModels(config);
52
+ setModelsLoaded(true);
53
+ setStatus('detecting');
54
+ running.current = true;
55
+ const detect = async () => {
56
+ if (!running.current || !videoRef.current)
57
+ return;
58
+ const video = videoRef.current;
59
+ if (video.readyState < 2) {
60
+ timerRef.current = setTimeout(detect, interval);
61
+ return;
62
+ }
63
+ const det = await faceApi.detectFace(video, config);
64
+ setDetection(det);
65
+ if (det) {
66
+ setStatus('detected');
67
+ if (doExtract) {
68
+ const desc = await faceApi.extractDescriptor(video, config);
69
+ setDescriptor(desc);
70
+ }
71
+ }
72
+ else {
73
+ setStatus('noFace');
74
+ setDescriptor(null);
75
+ }
76
+ if (running.current) {
77
+ timerRef.current = setTimeout(detect, interval);
78
+ }
79
+ };
80
+ detect();
81
+ }, [videoRef, interval, doExtract, config]);
82
+ (0, react_1.useEffect)(() => {
83
+ if (autoStart)
84
+ loadAndStart();
85
+ return () => {
86
+ running.current = false;
87
+ if (timerRef.current)
88
+ clearTimeout(timerRef.current);
89
+ };
90
+ }, [autoStart, loadAndStart]);
91
+ const stop = (0, react_1.useCallback)(() => {
92
+ running.current = false;
93
+ if (timerRef.current)
94
+ clearTimeout(timerRef.current);
95
+ }, []);
96
+ return { detection, descriptor, status, modelsLoaded, stop, start: loadAndStart };
97
+ }
@@ -0,0 +1,6 @@
1
+ export { loadModels, isLoaded, detectFace, detectAllFaces, extractDescriptor } from './lib/face-api';
2
+ export { compareFaces, findMatch, findAllMatches } from './lib/face-matcher';
3
+ export { descriptorToArray, arrayToDescriptor, isValidDescriptor, drawDetection } from './lib/face-utils';
4
+ export { useCamera } from './hooks/useCamera';
5
+ export { useFaceDetection } from './hooks/useFaceDetection';
6
+ export type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ // @mosta/face — Barrel exports
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ 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
+ // Core face-api service
7
+ var face_api_1 = require("./lib/face-api");
8
+ Object.defineProperty(exports, "loadModels", { enumerable: true, get: function () { return face_api_1.loadModels; } });
9
+ Object.defineProperty(exports, "isLoaded", { enumerable: true, get: function () { return face_api_1.isLoaded; } });
10
+ Object.defineProperty(exports, "detectFace", { enumerable: true, get: function () { return face_api_1.detectFace; } });
11
+ Object.defineProperty(exports, "detectAllFaces", { enumerable: true, get: function () { return face_api_1.detectAllFaces; } });
12
+ Object.defineProperty(exports, "extractDescriptor", { enumerable: true, get: function () { return face_api_1.extractDescriptor; } });
13
+ // Matching
14
+ var face_matcher_1 = require("./lib/face-matcher");
15
+ Object.defineProperty(exports, "compareFaces", { enumerable: true, get: function () { return face_matcher_1.compareFaces; } });
16
+ Object.defineProperty(exports, "findMatch", { enumerable: true, get: function () { return face_matcher_1.findMatch; } });
17
+ Object.defineProperty(exports, "findAllMatches", { enumerable: true, get: function () { return face_matcher_1.findAllMatches; } });
18
+ // Utils
19
+ var face_utils_1 = require("./lib/face-utils");
20
+ Object.defineProperty(exports, "descriptorToArray", { enumerable: true, get: function () { return face_utils_1.descriptorToArray; } });
21
+ Object.defineProperty(exports, "arrayToDescriptor", { enumerable: true, get: function () { return face_utils_1.arrayToDescriptor; } });
22
+ Object.defineProperty(exports, "isValidDescriptor", { enumerable: true, get: function () { return face_utils_1.isValidDescriptor; } });
23
+ Object.defineProperty(exports, "drawDetection", { enumerable: true, get: function () { return face_utils_1.drawDetection; } });
24
+ // Hooks
25
+ var useCamera_1 = require("./hooks/useCamera");
26
+ Object.defineProperty(exports, "useCamera", { enumerable: true, get: function () { return useCamera_1.useCamera; } });
27
+ var useFaceDetection_1 = require("./hooks/useFaceDetection");
28
+ Object.defineProperty(exports, "useFaceDetection", { enumerable: true, get: function () { return useFaceDetection_1.useFaceDetection; } });
@@ -0,0 +1,23 @@
1
+ import type { MostaFaceConfig } from '../types';
2
+ /**
3
+ * Load the face-api.js library and 3 models (TinyFaceDetector, landmarks68, recognition).
4
+ */
5
+ export declare function loadModels(config?: MostaFaceConfig): Promise<void>;
6
+ /** Check if models are loaded */
7
+ export declare function isLoaded(): boolean;
8
+ /**
9
+ * Detect a single face with landmarks.
10
+ */
11
+ export declare function detectFace(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<import("@vladmandic/face-api").WithFaceLandmarks<{
12
+ detection: import("@vladmandic/face-api").FaceDetection;
13
+ }, import("@vladmandic/face-api").FaceLandmarks68> | null>;
14
+ /**
15
+ * Detect all faces with landmarks.
16
+ */
17
+ export declare function detectAllFaces(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<import("@vladmandic/face-api").WithFaceLandmarks<{
18
+ detection: import("@vladmandic/face-api").FaceDetection;
19
+ }, import("@vladmandic/face-api").FaceLandmarks68>[]>;
20
+ /**
21
+ * Extract the 128-dimensional face descriptor from a single face.
22
+ */
23
+ export declare function extractDescriptor(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<Float32Array | null>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ // @mosta/face — Core face-api.js service (CLIENT-SIDE only)
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ //
5
+ // Usage: const faceApi = await import('@mosta/face') — always use dynamic import in useEffect
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.loadModels = loadModels;
41
+ exports.isLoaded = isLoaded;
42
+ exports.detectFace = detectFace;
43
+ exports.detectAllFaces = detectAllFaces;
44
+ exports.extractDescriptor = extractDescriptor;
45
+ let fapi = null;
46
+ let modelsLoaded = false;
47
+ /**
48
+ * Load the face-api.js library and 3 models (TinyFaceDetector, landmarks68, recognition).
49
+ */
50
+ async function loadModels(config) {
51
+ if (modelsLoaded)
52
+ return;
53
+ fapi = await Promise.resolve().then(() => __importStar(require('@vladmandic/face-api')));
54
+ const modelUrl = config?.modelsPath ?? '/models/face-api';
55
+ await Promise.all([
56
+ fapi.nets.tinyFaceDetector.loadFromUri(modelUrl),
57
+ fapi.nets.faceLandmark68Net.loadFromUri(modelUrl),
58
+ fapi.nets.faceRecognitionNet.loadFromUri(modelUrl),
59
+ ]);
60
+ modelsLoaded = true;
61
+ }
62
+ /** Check if models are loaded */
63
+ function isLoaded() {
64
+ return modelsLoaded;
65
+ }
66
+ /**
67
+ * Detect a single face with landmarks.
68
+ */
69
+ async function detectFace(input, config) {
70
+ if (!fapi)
71
+ throw new Error('face-api not loaded — call loadModels() first');
72
+ const detection = await fapi
73
+ .detectSingleFace(input, new fapi.TinyFaceDetectorOptions({
74
+ inputSize: config?.inputSize ?? 320,
75
+ scoreThreshold: config?.scoreThreshold ?? 0.5,
76
+ }))
77
+ .withFaceLandmarks();
78
+ return detection || null;
79
+ }
80
+ /**
81
+ * Detect all faces with landmarks.
82
+ */
83
+ async function detectAllFaces(input, config) {
84
+ if (!fapi)
85
+ throw new Error('face-api not loaded — call loadModels() first');
86
+ return fapi
87
+ .detectAllFaces(input, new fapi.TinyFaceDetectorOptions({
88
+ inputSize: config?.inputSize ?? 320,
89
+ scoreThreshold: config?.scoreThreshold ?? 0.5,
90
+ }))
91
+ .withFaceLandmarks();
92
+ }
93
+ /**
94
+ * Extract the 128-dimensional face descriptor from a single face.
95
+ */
96
+ async function extractDescriptor(input, config) {
97
+ if (!fapi)
98
+ throw new Error('face-api not loaded — call loadModels() first');
99
+ const result = await fapi
100
+ .detectSingleFace(input, new fapi.TinyFaceDetectorOptions({
101
+ inputSize: config?.inputSize ?? 320,
102
+ scoreThreshold: config?.scoreThreshold ?? 0.5,
103
+ }))
104
+ .withFaceLandmarks()
105
+ .withFaceDescriptor();
106
+ return result?.descriptor || null;
107
+ }
@@ -0,0 +1,18 @@
1
+ import type { FaceDescriptor, FaceMatchResult } from '../types';
2
+ /**
3
+ * Compute Euclidean distance between two 128-dim face descriptors.
4
+ */
5
+ export declare function compareFaces(d1: FaceDescriptor, d2: FaceDescriptor): number;
6
+ /**
7
+ * Find the best match among candidates.
8
+ * Returns null if no match is below the threshold.
9
+ */
10
+ export declare function findMatch<T extends {
11
+ faceDescriptor: number[];
12
+ }>(descriptor: FaceDescriptor, candidates: T[], threshold?: number): FaceMatchResult<T> | null;
13
+ /**
14
+ * Find all matches below the threshold, sorted by distance.
15
+ */
16
+ export declare function findAllMatches<T extends {
17
+ faceDescriptor: number[];
18
+ }>(descriptor: FaceDescriptor, candidates: T[], threshold?: number): FaceMatchResult<T>[];
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareFaces = compareFaces;
4
+ exports.findMatch = findMatch;
5
+ exports.findAllMatches = findAllMatches;
6
+ /**
7
+ * Compute Euclidean distance between two 128-dim face descriptors.
8
+ */
9
+ function compareFaces(d1, d2) {
10
+ const a = d1 instanceof Float32Array ? d1 : new Float32Array(d1);
11
+ const b = d2 instanceof Float32Array ? d2 : new Float32Array(d2);
12
+ if (a.length !== 128 || b.length !== 128) {
13
+ throw new Error('Descriptors must be 128 elements');
14
+ }
15
+ let sum = 0;
16
+ for (let i = 0; i < 128; i++) {
17
+ const diff = a[i] - b[i];
18
+ sum += diff * diff;
19
+ }
20
+ return Math.sqrt(sum);
21
+ }
22
+ /**
23
+ * Find the best match among candidates.
24
+ * Returns null if no match is below the threshold.
25
+ */
26
+ function findMatch(descriptor, candidates, threshold = 0.6) {
27
+ let bestMatch = null;
28
+ let bestDistance = Infinity;
29
+ for (const candidate of candidates) {
30
+ if (!candidate.faceDescriptor || candidate.faceDescriptor.length !== 128)
31
+ continue;
32
+ const distance = compareFaces(descriptor, candidate.faceDescriptor);
33
+ if (distance < bestDistance) {
34
+ bestDistance = distance;
35
+ bestMatch = candidate;
36
+ }
37
+ }
38
+ if (bestMatch && bestDistance < threshold) {
39
+ return { match: bestMatch, distance: bestDistance };
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Find all matches below the threshold, sorted by distance.
45
+ */
46
+ function findAllMatches(descriptor, candidates, threshold = 0.6) {
47
+ const results = [];
48
+ for (const candidate of candidates) {
49
+ if (!candidate.faceDescriptor || candidate.faceDescriptor.length !== 128)
50
+ continue;
51
+ const distance = compareFaces(descriptor, candidate.faceDescriptor);
52
+ if (distance < threshold) {
53
+ results.push({ match: candidate, distance });
54
+ }
55
+ }
56
+ return results.sort((a, b) => a.distance - b.distance);
57
+ }
@@ -0,0 +1,20 @@
1
+ /** Convert Float32Array descriptor to number[] for JSON/DB storage */
2
+ export declare function descriptorToArray(descriptor: Float32Array): number[];
3
+ /** Convert number[] back to Float32Array for comparison */
4
+ export declare function arrayToDescriptor(arr: number[]): Float32Array;
5
+ /** Validate that a descriptor is valid (128 elements) */
6
+ export declare function isValidDescriptor(data: unknown): boolean;
7
+ /**
8
+ * Draw a detection bounding box + score on a canvas overlay.
9
+ */
10
+ export declare function drawDetection(canvas: HTMLCanvasElement, detection: {
11
+ detection: {
12
+ score: number;
13
+ box: {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ };
20
+ } | null, videoWidth: number, videoHeight: number): void;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // @mosta/face — Utility functions
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.descriptorToArray = descriptorToArray;
6
+ exports.arrayToDescriptor = arrayToDescriptor;
7
+ exports.isValidDescriptor = isValidDescriptor;
8
+ exports.drawDetection = drawDetection;
9
+ /** Convert Float32Array descriptor to number[] for JSON/DB storage */
10
+ function descriptorToArray(descriptor) {
11
+ return Array.from(descriptor);
12
+ }
13
+ /** Convert number[] back to Float32Array for comparison */
14
+ function arrayToDescriptor(arr) {
15
+ return new Float32Array(arr);
16
+ }
17
+ /** Validate that a descriptor is valid (128 elements) */
18
+ function isValidDescriptor(data) {
19
+ if (data instanceof Float32Array)
20
+ return data.length === 128;
21
+ if (Array.isArray(data))
22
+ return data.length === 128 && data.every((v) => typeof v === 'number');
23
+ return false;
24
+ }
25
+ /**
26
+ * Draw a detection bounding box + score on a canvas overlay.
27
+ */
28
+ function drawDetection(canvas, detection, videoWidth, videoHeight) {
29
+ const ctx = canvas.getContext('2d');
30
+ if (!ctx)
31
+ return;
32
+ canvas.width = videoWidth;
33
+ canvas.height = videoHeight;
34
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
35
+ if (!detection)
36
+ return;
37
+ const box = detection.detection.box;
38
+ ctx.strokeStyle = '#22c55e';
39
+ ctx.lineWidth = 3;
40
+ ctx.strokeRect(box.x, box.y, box.width, box.height);
41
+ const score = Math.round(detection.detection.score * 100);
42
+ ctx.fillStyle = '#22c55e';
43
+ ctx.font = 'bold 14px sans-serif';
44
+ ctx.fillText(`Visage ${score}%`, box.x, box.y - 6);
45
+ }
@@ -0,0 +1,27 @@
1
+ export interface MostaFaceConfig {
2
+ /** Path to face-api.js model files (default: '/models/face-api') */
3
+ modelsPath?: string;
4
+ /** Detection score threshold (default: 0.5) */
5
+ scoreThreshold?: number;
6
+ /** TinyFaceDetector input size (default: 320) */
7
+ inputSize?: number;
8
+ /** Face matching distance threshold (default: 0.6) */
9
+ matchThreshold?: number;
10
+ }
11
+ export interface FaceDetectionResult {
12
+ detection: {
13
+ score: number;
14
+ box: {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ };
20
+ };
21
+ landmarks?: any;
22
+ }
23
+ export interface FaceMatchResult<T> {
24
+ match: T;
25
+ distance: number;
26
+ }
27
+ export type FaceDescriptor = Float32Array | number[];
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // @mosta/face — Types
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@mostajs/face",
3
+ "version": "1.0.0",
4
+ "description": "Reusable face recognition module — detection, descriptor extraction, 1:N matching",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./hooks/useCamera": {
17
+ "types": "./dist/hooks/useCamera.d.ts",
18
+ "import": "./dist/hooks/useCamera.js",
19
+ "require": "./dist/hooks/useCamera.js",
20
+ "default": "./dist/hooks/useCamera.js"
21
+ },
22
+ "./hooks/useFaceDetection": {
23
+ "types": "./dist/hooks/useFaceDetection.d.ts",
24
+ "import": "./dist/hooks/useFaceDetection.js",
25
+ "require": "./dist/hooks/useFaceDetection.js",
26
+ "default": "./dist/hooks/useFaceDetection.js"
27
+ }
28
+ },
29
+ "files": ["dist", "LICENSE", "README.md"],
30
+ "keywords": ["face-recognition", "face-detection", "face-api", "biometrics", "react", "mosta"],
31
+ "repository": { "type": "git", "url": "https://github.com/apolocine/mosta-face" },
32
+ "homepage": "https://mostajs.dev/packages/face",
33
+ "engines": { "node": ">=18.0.0" },
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "@vladmandic/face-api": "^1.7.0"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=18"
43
+ },
44
+ "devDependencies": {
45
+ "@types/react": "^19.0.0",
46
+ "react": "^19.0.0",
47
+ "typescript": "^5.6.0"
48
+ }
49
+ }