@mostajs/face 1.0.0 → 1.1.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/README.md +387 -23
- package/dist/api/detect.route.d.ts +24 -0
- package/dist/api/detect.route.js +39 -0
- package/dist/api/recognize.route.d.ts +49 -0
- package/dist/api/recognize.route.js +106 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -1
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# @mostajs/face
|
|
2
2
|
|
|
3
|
-
> Reusable face recognition module — detection, descriptor extraction, 1:N matching.
|
|
3
|
+
> Reusable face recognition module — detection, descriptor extraction, 1:N matching, API route factories.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@mostajs/face)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Part of the [@mosta suite](https://mostajs.dev).
|
|
8
|
+
Part of the [@mosta suite](https://mostajs.dev). **100% standalone** — zero dependency on `@mostajs/orm` or any other `@mostajs/*` package.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table des matieres
|
|
13
|
+
|
|
14
|
+
1. [Installation](#installation)
|
|
15
|
+
2. [Quick Start](#quick-start)
|
|
16
|
+
3. [API Route Factories](#api-route-factories)
|
|
17
|
+
4. [React Hooks](#react-hooks)
|
|
18
|
+
5. [Integration complete dans une nouvelle app](#integration-complete)
|
|
19
|
+
6. [API Reference](#api-reference)
|
|
20
|
+
7. [Architecture](#architecture)
|
|
9
21
|
|
|
10
22
|
---
|
|
11
23
|
|
|
@@ -15,59 +27,411 @@ Part of the [@mosta suite](https://mostajs.dev).
|
|
|
15
27
|
npm install @mostajs/face
|
|
16
28
|
```
|
|
17
29
|
|
|
30
|
+
Copier les modeles face-api.js dans votre dossier `public/` :
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
mkdir -p public/models/face-api
|
|
34
|
+
# Copier les fichiers .bin et .json depuis node_modules/@vladmandic/face-api/model/
|
|
35
|
+
cp node_modules/@vladmandic/face-api/model/tiny_face_detector_model-* public/models/face-api/
|
|
36
|
+
cp node_modules/@vladmandic/face-api/model/face_landmark_68_model-* public/models/face-api/
|
|
37
|
+
cp node_modules/@vladmandic/face-api/model/face_recognition_model-* public/models/face-api/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
18
42
|
## Quick Start
|
|
19
43
|
|
|
20
|
-
### 1.
|
|
44
|
+
### 1. Charger les modeles et detecter des visages
|
|
21
45
|
|
|
22
46
|
```typescript
|
|
23
47
|
import { loadModels, detectFace, extractDescriptor } from '@mostajs/face'
|
|
24
48
|
|
|
25
|
-
await loadModels('/models
|
|
49
|
+
await loadModels('/models/face-api')
|
|
26
50
|
|
|
27
51
|
const detection = await detectFace(imageElement)
|
|
28
|
-
const descriptor = await extractDescriptor(imageElement)
|
|
52
|
+
const descriptor = await extractDescriptor(imageElement) // Float32Array(128)
|
|
29
53
|
```
|
|
30
54
|
|
|
31
|
-
### 2.
|
|
55
|
+
### 2. Comparer des visages
|
|
32
56
|
|
|
33
57
|
```typescript
|
|
34
58
|
import { compareFaces, findMatch } from '@mostajs/face'
|
|
35
59
|
|
|
60
|
+
// Distance euclidienne entre deux descripteurs
|
|
36
61
|
const distance = compareFaces(descriptor1, descriptor2)
|
|
62
|
+
console.log(distance < 0.6 ? 'Meme personne' : 'Personnes differentes')
|
|
37
63
|
|
|
38
|
-
|
|
64
|
+
// Recherche 1:N parmi des candidats
|
|
65
|
+
const candidates = [
|
|
66
|
+
{ id: '1', faceDescriptor: [...], name: 'Alice' },
|
|
67
|
+
{ id: '2', faceDescriptor: [...], name: 'Bob' },
|
|
68
|
+
]
|
|
69
|
+
const match = findMatch(unknownDescriptor, candidates, 0.6)
|
|
39
70
|
if (match) {
|
|
40
|
-
console.log('
|
|
71
|
+
console.log('Match:', match.match.name, 'distance:', match.distance)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## API Route Factories
|
|
78
|
+
|
|
79
|
+
Depuis la v1.1.0, le package fournit des factories pour creer des routes API (Next.js App Router ou tout framework supportant `Request`/`Response`).
|
|
80
|
+
|
|
81
|
+
### POST /api/face/recognize
|
|
82
|
+
|
|
83
|
+
Factory qui recoit un descripteur facial et cherche le meilleur match parmi vos candidats.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// src/app/api/face/recognize/route.ts
|
|
87
|
+
import { createRecognizeHandler } from '@mostajs/face/api/recognize.route'
|
|
88
|
+
import { db } from '@/lib/db'
|
|
89
|
+
|
|
90
|
+
export const { POST } = createRecognizeHandler({
|
|
91
|
+
// Fournir les candidats depuis votre base de donnees
|
|
92
|
+
getCandidates: async () => {
|
|
93
|
+
return db.query('SELECT id, name, photo, "faceDescriptor" FROM users WHERE "faceDescriptor" IS NOT NULL')
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Seuil de matching (defaut: 0.6)
|
|
97
|
+
getThreshold: async () => 0.55,
|
|
98
|
+
|
|
99
|
+
// Champs retournes dans la reponse (defaut: tous sauf faceDescriptor)
|
|
100
|
+
publicFields: ['name', 'photo', 'email'],
|
|
101
|
+
|
|
102
|
+
// Optionnel: verifier l'authentification
|
|
103
|
+
checkAuth: async (req) => {
|
|
104
|
+
const token = req.headers.get('Authorization')
|
|
105
|
+
if (!token) return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
106
|
+
return null // OK
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Optionnel: desactiver la feature
|
|
110
|
+
isEnabled: async () => {
|
|
111
|
+
const settings = await getAppSettings()
|
|
112
|
+
return settings.faceRecognitionEnabled
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Requete :**
|
|
118
|
+
```json
|
|
119
|
+
POST /api/face/recognize
|
|
120
|
+
{ "faceDescriptor": [0.023, -0.114, 0.087, ...] }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Reponse (match) :**
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"data": {
|
|
127
|
+
"match": true,
|
|
128
|
+
"distance": 0.312,
|
|
129
|
+
"candidate": { "id": "abc123", "name": "Alice", "photo": "/photos/alice.jpg" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Reponse (pas de match) :**
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"data": { "match": false, "distance": 0.782 }
|
|
41
138
|
}
|
|
42
139
|
```
|
|
43
140
|
|
|
44
|
-
###
|
|
141
|
+
### POST /api/face/detect (placeholder)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// src/app/api/face/detect/route.ts
|
|
145
|
+
import { createDetectHandler } from '@mostajs/face/api/detect.route'
|
|
146
|
+
|
|
147
|
+
export const { POST } = createDetectHandler({
|
|
148
|
+
checkAuth: async (req) => { /* ... */ return null },
|
|
149
|
+
isEnabled: async () => true,
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> La detection se fait cote client via face-api.js. Cette route sert de point d'entree pour le controle d'acces et un futur traitement serveur.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## React Hooks
|
|
158
|
+
|
|
159
|
+
### useCamera
|
|
45
160
|
|
|
46
161
|
```tsx
|
|
47
162
|
import { useCamera } from '@mostajs/face/hooks/useCamera'
|
|
163
|
+
|
|
164
|
+
function CameraView() {
|
|
165
|
+
const { videoRef, streaming, start, stop, capture } = useCamera()
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<video ref={videoRef} autoPlay playsInline muted />
|
|
170
|
+
{!streaming
|
|
171
|
+
? <button onClick={start}>Demarrer</button>
|
|
172
|
+
: <button onClick={() => capture(canvas)}>Capturer</button>
|
|
173
|
+
}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### useFaceDetection
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
48
182
|
import { useFaceDetection } from '@mostajs/face/hooks/useFaceDetection'
|
|
49
183
|
|
|
50
|
-
function
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
|
|
184
|
+
function LiveDetection({ videoRef }) {
|
|
185
|
+
const { faceDetected, detection, descriptor } = useFaceDetection(videoRef, {
|
|
186
|
+
scoreThreshold: 0.5,
|
|
187
|
+
inputSize: 320,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return <span>{faceDetected ? 'Visage detecte' : 'Aucun visage'}</span>
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Integration complete
|
|
197
|
+
|
|
198
|
+
Exemple complet d'integration dans une nouvelle app Next.js.
|
|
199
|
+
|
|
200
|
+
### Etape 1 — Installer
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm install @mostajs/face
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Etape 2 — Copier les modeles
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
mkdir -p public/models/face-api
|
|
210
|
+
cp node_modules/@vladmandic/face-api/model/tiny_face_detector_model-* public/models/face-api/
|
|
211
|
+
cp node_modules/@vladmandic/face-api/model/face_landmark_68_model-* public/models/face-api/
|
|
212
|
+
cp node_modules/@vladmandic/face-api/model/face_recognition_model-* public/models/face-api/
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Etape 3 — Route API recognize
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// src/app/api/face/recognize/route.ts
|
|
219
|
+
import { createRecognizeHandler } from '@mostajs/face/api/recognize.route'
|
|
220
|
+
import prisma from '@/lib/prisma' // ou tout ORM
|
|
221
|
+
|
|
222
|
+
export const { POST } = createRecognizeHandler({
|
|
223
|
+
getCandidates: async () => {
|
|
224
|
+
const users = await prisma.user.findMany({
|
|
225
|
+
where: { faceDescriptor: { not: null } },
|
|
226
|
+
select: { id: true, name: true, photo: true, faceDescriptor: true },
|
|
227
|
+
})
|
|
228
|
+
return users.map(u => ({
|
|
229
|
+
...u,
|
|
230
|
+
faceDescriptor: u.faceDescriptor as number[],
|
|
231
|
+
}))
|
|
232
|
+
},
|
|
233
|
+
getThreshold: async () => 0.6,
|
|
234
|
+
publicFields: ['name', 'photo'],
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Etape 4 — Composant React
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
'use client'
|
|
242
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
243
|
+
import { useCamera } from '@mostajs/face/hooks/useCamera'
|
|
244
|
+
|
|
245
|
+
export default function FaceSearch() {
|
|
246
|
+
const { videoRef, streaming, start, stop } = useCamera()
|
|
247
|
+
const [result, setResult] = useState(null)
|
|
248
|
+
const faceApiRef = useRef(null)
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
import('@mostajs/face').then(mod => {
|
|
252
|
+
mod.loadModels('/models/face-api')
|
|
253
|
+
faceApiRef.current = mod
|
|
254
|
+
})
|
|
255
|
+
}, [])
|
|
256
|
+
|
|
257
|
+
const recognize = useCallback(async () => {
|
|
258
|
+
if (!faceApiRef.current || !videoRef.current) return
|
|
259
|
+
const descriptor = await faceApiRef.current.extractDescriptor(videoRef.current)
|
|
260
|
+
if (!descriptor) { alert('Aucun visage'); return }
|
|
261
|
+
|
|
262
|
+
const res = await fetch('/api/face/recognize', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ faceDescriptor: Array.from(descriptor) }),
|
|
266
|
+
})
|
|
267
|
+
const json = await res.json()
|
|
268
|
+
setResult(json.data)
|
|
269
|
+
}, [videoRef])
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div>
|
|
273
|
+
<video ref={videoRef} autoPlay playsInline muted />
|
|
274
|
+
{!streaming
|
|
275
|
+
? <button onClick={start}>Camera</button>
|
|
276
|
+
: <button onClick={recognize}>Rechercher</button>
|
|
277
|
+
}
|
|
278
|
+
{result?.match && <p>Trouve: {result.candidate.name}</p>}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
54
281
|
}
|
|
55
282
|
```
|
|
56
283
|
|
|
284
|
+
### Etape 5 — Verification locale
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# Demarrer l'app
|
|
288
|
+
npm run dev
|
|
289
|
+
|
|
290
|
+
# Tester l'API
|
|
291
|
+
curl -X POST http://localhost:3000/api/face/recognize \
|
|
292
|
+
-H 'Content-Type: application/json' \
|
|
293
|
+
-d '{"faceDescriptor": [0.1, 0.2, ...]}'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
57
298
|
## API Reference
|
|
58
299
|
|
|
300
|
+
### Core (client-side)
|
|
301
|
+
|
|
302
|
+
| Export | Description |
|
|
303
|
+
|--------|-------------|
|
|
304
|
+
| `loadModels(path)` | Charger les modeles face-api.js |
|
|
305
|
+
| `isLoaded()` | Verifier si les modeles sont charges |
|
|
306
|
+
| `detectFace(input)` | Detecter un visage avec landmarks |
|
|
307
|
+
| `detectAllFaces(input)` | Detecter tous les visages |
|
|
308
|
+
| `extractDescriptor(input)` | Extraire un descripteur 128-dim |
|
|
309
|
+
|
|
310
|
+
### Matching (client ou serveur)
|
|
311
|
+
|
|
312
|
+
| Export | Description |
|
|
313
|
+
|--------|-------------|
|
|
314
|
+
| `compareFaces(d1, d2)` | Distance euclidienne entre 2 descripteurs |
|
|
315
|
+
| `findMatch(descriptor, candidates, threshold)` | Meilleur match sous le seuil |
|
|
316
|
+
| `findAllMatches(descriptor, candidates, threshold)` | Tous les matchs sous le seuil |
|
|
317
|
+
|
|
318
|
+
### Utils
|
|
319
|
+
|
|
59
320
|
| Export | Description |
|
|
60
321
|
|--------|-------------|
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
322
|
+
| `descriptorToArray(Float32Array)` | Convertir en `number[]` |
|
|
323
|
+
| `arrayToDescriptor(number[])` | Convertir en `Float32Array` |
|
|
324
|
+
| `isValidDescriptor(value)` | Verifier (128 nombres) |
|
|
325
|
+
| `drawDetection(canvas, detection, w, h)` | Dessiner le cadre de detection |
|
|
326
|
+
|
|
327
|
+
### Route Factories (serveur)
|
|
328
|
+
|
|
329
|
+
| Export | Description |
|
|
330
|
+
|--------|-------------|
|
|
331
|
+
| `createRecognizeHandler(config)` | Factory POST recognize avec matching 1:N |
|
|
332
|
+
| `createDetectHandler(config?)` | Factory POST detect (placeholder) |
|
|
333
|
+
|
|
334
|
+
### React Hooks
|
|
335
|
+
|
|
336
|
+
| Export | Description |
|
|
337
|
+
|--------|-------------|
|
|
338
|
+
| `useCamera()` | Gestion camera (start/stop/capture) |
|
|
339
|
+
| `useFaceDetection(videoRef, config?)` | Detection continue en temps reel |
|
|
340
|
+
|
|
341
|
+
### Types
|
|
342
|
+
|
|
343
|
+
| Type | Description |
|
|
344
|
+
|------|-------------|
|
|
345
|
+
| `MostaFaceConfig` | Configuration (modelsPath, thresholds, inputSize) |
|
|
346
|
+
| `FaceDetectionResult` | Resultat detection (score, box) |
|
|
347
|
+
| `FaceMatchResult<T>` | Resultat matching (match, distance) |
|
|
348
|
+
| `FaceDescriptor` | `Float32Array \| number[]` |
|
|
349
|
+
| `FaceCandidate` | Interface candidat (id, faceDescriptor, ...) |
|
|
350
|
+
| `RecognizeHandlerConfig` | Config de `createRecognizeHandler` |
|
|
351
|
+
| `DetectHandlerConfig` | Config de `createDetectHandler` |
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Architecture
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
@mostajs/face (100% standalone)
|
|
359
|
+
├── lib/
|
|
360
|
+
│ ├── face-api.ts # Chargement modeles, detection, extraction
|
|
361
|
+
│ ├── face-matcher.ts # Comparaison, findMatch, findAllMatches
|
|
362
|
+
│ └── face-utils.ts # Dessin, conversion descripteurs
|
|
363
|
+
├── api/
|
|
364
|
+
│ ├── recognize.route.ts # Factory POST /api/face/recognize
|
|
365
|
+
│ └── detect.route.ts # Factory POST /api/face/detect
|
|
366
|
+
├── hooks/
|
|
367
|
+
│ ├── useCamera.ts # Hook gestion camera
|
|
368
|
+
│ └── useFaceDetection.ts # Hook detection temps reel
|
|
369
|
+
├── types/
|
|
370
|
+
│ └── index.ts # Types exports
|
|
371
|
+
└── index.ts # Barrel exports
|
|
372
|
+
|
|
373
|
+
Dependances:
|
|
374
|
+
@vladmandic/face-api (seule dep runtime)
|
|
375
|
+
react >= 18 (peer dependency)
|
|
376
|
+
|
|
377
|
+
Zero dependance sur: @mostajs/orm, @mostajs/auth, @mostajs/settings
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Pattern Factory
|
|
381
|
+
|
|
382
|
+
Le meme pattern que `@mostajs/setup` : le package exporte des factories, l'app injecte ses dependances :
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
┌─────────────────────┐ inject callbacks ┌──────────────────────┐
|
|
386
|
+
│ @mostajs/face │ ◄──────────────────────── │ Votre app │
|
|
387
|
+
│ │ │ │
|
|
388
|
+
│ createRecognizeHandler({ │ getCandidates: () => │
|
|
389
|
+
│ getCandidates, │ │ db.query(...) │
|
|
390
|
+
│ checkAuth, │ │ checkAuth: () => │
|
|
391
|
+
│ getThreshold, │ │ verifyToken(...) │
|
|
392
|
+
│ }) │ │ │
|
|
393
|
+
└─────────────────────┘ └──────────────────────┘
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
Résultat
|
|
399
|
+
|
|
400
|
+
Ce qui a été migré dans @mostajs/face v1.1.0
|
|
401
|
+
|
|
402
|
+
┌────────────────────────┬─────────────────────────────────────────────────────┐
|
|
403
|
+
│ Fichier │ Description │
|
|
404
|
+
├────────────────────────┼─────────────────────────────────────────────────────┤
|
|
405
|
+
│ api/recognize.route.ts │ Factory createRecognizeHandler() — matching 1:N │
|
|
406
|
+
│ │ avec callbacks injectés │
|
|
407
|
+
├────────────────────────┼─────────────────────────────────────────────────────┤
|
|
408
|
+
│ api/detect.route.ts │ Factory createDetectHandler() — placeholder avec │
|
|
409
|
+
│ │ auth/enable │
|
|
410
|
+
├────────────────────────┼─────────────────────────────────────────────────────┤
|
|
411
|
+
│ index.ts │ +4 exports (2 factories + 2 types) │
|
|
412
|
+
├────────────────────────┼─────────────────────────────────────────────────────┤
|
|
413
|
+
│ package.json │ v1.1.0, subpath exports ./api/*, ./lib/*, ./types │
|
|
414
|
+
├────────────────────────┼─────────────────────────────────────────────────────┤
|
|
415
|
+
│ README.md │ Tutoriel complet avec intégration nouvelle app │
|
|
416
|
+
└────────────────────────┴─────────────────────────────────────────────────────┘
|
|
417
|
+
|
|
418
|
+
Routes app simplifiées
|
|
419
|
+
|
|
420
|
+
- src/app/api/face/recognize/route.ts : 101 lignes → 30 lignes (injection de
|
|
421
|
+
getCandidates, checkAuth, getThreshold, publicFields)
|
|
422
|
+
- src/app/api/face/detect/route.ts : 30 lignes → 15 lignes
|
|
423
|
+
|
|
424
|
+
Ce qui reste dans l'app (non migré)
|
|
425
|
+
|
|
426
|
+
- src/app/api/scan/route.ts — Scanning de tickets (QR, quotas, réentrée). Pas lié
|
|
427
|
+
à face, relève d'un futur @mostajs/ticketing
|
|
428
|
+
- src/components/clients/FaceDetector.tsx — Composant UI trop couplé (shadcn,
|
|
429
|
+
sonner, settings). Les hooks useCamera/useFaceDetection du package couvrent la
|
|
430
|
+
partie réutilisable
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
---
|
|
70
434
|
|
|
71
435
|
## License
|
|
72
436
|
|
|
73
|
-
MIT —
|
|
437
|
+
MIT — Dr Hamid MADANI <drmdh@msn.com>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the detect handler.
|
|
3
|
+
*/
|
|
4
|
+
export interface DetectHandlerConfig {
|
|
5
|
+
/** Check auth/permission — return null if OK, or a Response to deny */
|
|
6
|
+
checkAuth?: (req: Request) => Promise<Response | null>;
|
|
7
|
+
/** Check if face recognition is enabled (default: always true) */
|
|
8
|
+
isEnabled?: () => Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Factory for POST /api/face/detect
|
|
12
|
+
*
|
|
13
|
+
* Placeholder endpoint — face detection runs client-side via face-api.js.
|
|
14
|
+
* This route exists for permission gating and future server-side detection.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { createDetectHandler } from '@mostajs/face/api/detect.route'
|
|
19
|
+
* export const { POST } = createDetectHandler()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function createDetectHandler(config?: DetectHandlerConfig): {
|
|
23
|
+
POST: (req: Request) => Promise<Response>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — Detect API route factory (placeholder)
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.createDetectHandler = createDetectHandler;
|
|
6
|
+
/**
|
|
7
|
+
* Factory for POST /api/face/detect
|
|
8
|
+
*
|
|
9
|
+
* Placeholder endpoint — face detection runs client-side via face-api.js.
|
|
10
|
+
* This route exists for permission gating and future server-side detection.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createDetectHandler } from '@mostajs/face/api/detect.route'
|
|
15
|
+
* export const { POST } = createDetectHandler()
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function createDetectHandler(config = {}) {
|
|
19
|
+
async function POST(req) {
|
|
20
|
+
if (config.isEnabled) {
|
|
21
|
+
const enabled = await config.isEnabled();
|
|
22
|
+
if (!enabled) {
|
|
23
|
+
return Response.json({ error: { code: 'FEATURE_DISABLED', message: 'Face recognition is disabled' } }, { status: 403 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (config.checkAuth) {
|
|
27
|
+
const denied = await config.checkAuth(req);
|
|
28
|
+
if (denied)
|
|
29
|
+
return denied;
|
|
30
|
+
}
|
|
31
|
+
return Response.json({
|
|
32
|
+
data: {
|
|
33
|
+
message: 'Face detection runs client-side. Use the FaceDetector component or useFaceDetection hook.',
|
|
34
|
+
hint: 'For recognition, send the descriptor to POST /api/face/recognize',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { POST };
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate shape: must have an id, faceDescriptor, and any extra fields.
|
|
3
|
+
*/
|
|
4
|
+
export interface FaceCandidate {
|
|
5
|
+
id: string;
|
|
6
|
+
faceDescriptor: number[];
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for the recognize handler.
|
|
11
|
+
*/
|
|
12
|
+
export interface RecognizeHandlerConfig {
|
|
13
|
+
/** Fetch all active candidates with a faceDescriptor from the database */
|
|
14
|
+
getCandidates: () => Promise<FaceCandidate[]>;
|
|
15
|
+
/** Check auth/permission — return null if OK, or a Response to deny */
|
|
16
|
+
checkAuth?: (req: Request) => Promise<Response | null>;
|
|
17
|
+
/** Get the matching distance threshold (default: 0.6) */
|
|
18
|
+
getThreshold?: () => Promise<number>;
|
|
19
|
+
/** Check if face recognition is enabled (default: always true) */
|
|
20
|
+
isEnabled?: () => Promise<boolean>;
|
|
21
|
+
/** Fields to return from the matched candidate (default: all except faceDescriptor) */
|
|
22
|
+
publicFields?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Factory for POST /api/face/recognize
|
|
26
|
+
*
|
|
27
|
+
* Receives a 128-float face descriptor, searches candidates, returns best match.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // app route: src/app/api/face/recognize/route.ts
|
|
32
|
+
* import { createRecognizeHandler } from '@mostajs/face/api/recognize.route'
|
|
33
|
+
* import { clientRepo } from '@/dal/service'
|
|
34
|
+
*
|
|
35
|
+
* export const { POST } = createRecognizeHandler({
|
|
36
|
+
* getCandidates: async () => {
|
|
37
|
+
* const repo = await clientRepo()
|
|
38
|
+
* return repo.findAll(
|
|
39
|
+
* { faceDescriptor: { $exists: true, $not: { $size: 0 } }, status: 'active' },
|
|
40
|
+
* { select: ['firstName', 'lastName', 'clientNumber', 'photo', 'faceDescriptor'] },
|
|
41
|
+
* )
|
|
42
|
+
* },
|
|
43
|
+
* getThreshold: async () => 0.6,
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createRecognizeHandler(config: RecognizeHandlerConfig): {
|
|
48
|
+
POST: (req: Request) => Promise<Response>;
|
|
49
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRecognizeHandler = createRecognizeHandler;
|
|
4
|
+
// @mosta/face — Recognize API route factory
|
|
5
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
6
|
+
const face_matcher_1 = require("../lib/face-matcher");
|
|
7
|
+
/**
|
|
8
|
+
* Factory for POST /api/face/recognize
|
|
9
|
+
*
|
|
10
|
+
* Receives a 128-float face descriptor, searches candidates, returns best match.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // app route: src/app/api/face/recognize/route.ts
|
|
15
|
+
* import { createRecognizeHandler } from '@mostajs/face/api/recognize.route'
|
|
16
|
+
* import { clientRepo } from '@/dal/service'
|
|
17
|
+
*
|
|
18
|
+
* export const { POST } = createRecognizeHandler({
|
|
19
|
+
* getCandidates: async () => {
|
|
20
|
+
* const repo = await clientRepo()
|
|
21
|
+
* return repo.findAll(
|
|
22
|
+
* { faceDescriptor: { $exists: true, $not: { $size: 0 } }, status: 'active' },
|
|
23
|
+
* { select: ['firstName', 'lastName', 'clientNumber', 'photo', 'faceDescriptor'] },
|
|
24
|
+
* )
|
|
25
|
+
* },
|
|
26
|
+
* getThreshold: async () => 0.6,
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function createRecognizeHandler(config) {
|
|
31
|
+
async function POST(req) {
|
|
32
|
+
// Check if enabled
|
|
33
|
+
if (config.isEnabled) {
|
|
34
|
+
const enabled = await config.isEnabled();
|
|
35
|
+
if (!enabled) {
|
|
36
|
+
return Response.json({ error: { code: 'FEATURE_DISABLED', message: 'Face recognition is disabled' } }, { status: 403 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Check auth
|
|
40
|
+
if (config.checkAuth) {
|
|
41
|
+
const denied = await config.checkAuth(req);
|
|
42
|
+
if (denied)
|
|
43
|
+
return denied;
|
|
44
|
+
}
|
|
45
|
+
// Parse body
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = await req.json();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return Response.json({ error: { code: 'VALIDATION_ERROR', message: 'Invalid JSON' } }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
// Validate descriptor
|
|
54
|
+
const desc = body.faceDescriptor;
|
|
55
|
+
if (!Array.isArray(desc) ||
|
|
56
|
+
desc.length !== 128 ||
|
|
57
|
+
!desc.every((v) => typeof v === 'number')) {
|
|
58
|
+
return Response.json({ error: { code: 'VALIDATION_ERROR', message: 'faceDescriptor must be an array of 128 numbers' } }, { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
// Get candidates
|
|
61
|
+
const candidates = await config.getCandidates();
|
|
62
|
+
if (candidates.length === 0) {
|
|
63
|
+
return Response.json({
|
|
64
|
+
data: { match: false, message: 'No candidates with face data' },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Find match
|
|
68
|
+
const threshold = config.getThreshold ? await config.getThreshold() : 0.6;
|
|
69
|
+
const result = (0, face_matcher_1.findMatch)(desc, candidates, threshold);
|
|
70
|
+
if (result) {
|
|
71
|
+
// Strip faceDescriptor from response
|
|
72
|
+
const { faceDescriptor: _fd, ...publicData } = result.match;
|
|
73
|
+
const filtered = config.publicFields
|
|
74
|
+
? Object.fromEntries(Object.entries(publicData).filter(([k]) => config.publicFields.includes(k) || k === 'id'))
|
|
75
|
+
: publicData;
|
|
76
|
+
return Response.json({
|
|
77
|
+
data: {
|
|
78
|
+
match: true,
|
|
79
|
+
distance: Math.round(result.distance * 1000) / 1000,
|
|
80
|
+
candidate: filtered,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// No match — still report best distance for debugging
|
|
85
|
+
let bestDistance = null;
|
|
86
|
+
for (const c of candidates) {
|
|
87
|
+
if (!c.faceDescriptor || c.faceDescriptor.length !== 128)
|
|
88
|
+
continue;
|
|
89
|
+
let sum = 0;
|
|
90
|
+
for (let i = 0; i < 128; i++) {
|
|
91
|
+
const diff = desc[i] - c.faceDescriptor[i];
|
|
92
|
+
sum += diff * diff;
|
|
93
|
+
}
|
|
94
|
+
const d = Math.sqrt(sum);
|
|
95
|
+
if (bestDistance === null || d < bestDistance)
|
|
96
|
+
bestDistance = d;
|
|
97
|
+
}
|
|
98
|
+
return Response.json({
|
|
99
|
+
data: {
|
|
100
|
+
match: false,
|
|
101
|
+
distance: bestDistance !== null ? Math.round(bestDistance * 1000) / 1000 : null,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { POST };
|
|
106
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,8 @@ export { compareFaces, findMatch, findAllMatches } from './lib/face-matcher';
|
|
|
3
3
|
export { descriptorToArray, arrayToDescriptor, isValidDescriptor, drawDetection } from './lib/face-utils';
|
|
4
4
|
export { useCamera } from './hooks/useCamera';
|
|
5
5
|
export { useFaceDetection } from './hooks/useFaceDetection';
|
|
6
|
+
export { createRecognizeHandler } from './api/recognize.route';
|
|
7
|
+
export { createDetectHandler } from './api/detect.route';
|
|
6
8
|
export type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor } from './types';
|
|
9
|
+
export type { FaceCandidate, RecognizeHandlerConfig } from './api/recognize.route';
|
|
10
|
+
export type { DetectHandlerConfig } from './api/detect.route';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @mosta/face — Barrel exports
|
|
3
3
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
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;
|
|
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;
|
|
6
6
|
// Core face-api service
|
|
7
7
|
var face_api_1 = require("./lib/face-api");
|
|
8
8
|
Object.defineProperty(exports, "loadModels", { enumerable: true, get: function () { return face_api_1.loadModels; } });
|
|
@@ -26,3 +26,8 @@ var useCamera_1 = require("./hooks/useCamera");
|
|
|
26
26
|
Object.defineProperty(exports, "useCamera", { enumerable: true, get: function () { return useCamera_1.useCamera; } });
|
|
27
27
|
var useFaceDetection_1 = require("./hooks/useFaceDetection");
|
|
28
28
|
Object.defineProperty(exports, "useFaceDetection", { enumerable: true, get: function () { return useFaceDetection_1.useFaceDetection; } });
|
|
29
|
+
// API route factories
|
|
30
|
+
var recognize_route_1 = require("./api/recognize.route");
|
|
31
|
+
Object.defineProperty(exports, "createRecognizeHandler", { enumerable: true, get: function () { return recognize_route_1.createRecognizeHandler; } });
|
|
32
|
+
var detect_route_1 = require("./api/detect.route");
|
|
33
|
+
Object.defineProperty(exports, "createDetectHandler", { enumerable: true, get: function () { return detect_route_1.createDetectHandler; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/face",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -24,6 +24,21 @@
|
|
|
24
24
|
"import": "./dist/hooks/useFaceDetection.js",
|
|
25
25
|
"require": "./dist/hooks/useFaceDetection.js",
|
|
26
26
|
"default": "./dist/hooks/useFaceDetection.js"
|
|
27
|
+
},
|
|
28
|
+
"./lib/*": {
|
|
29
|
+
"types": "./dist/lib/*.d.ts",
|
|
30
|
+
"import": "./dist/lib/*.js",
|
|
31
|
+
"default": "./dist/lib/*.js"
|
|
32
|
+
},
|
|
33
|
+
"./api/*": {
|
|
34
|
+
"types": "./dist/api/*.d.ts",
|
|
35
|
+
"import": "./dist/api/*.js",
|
|
36
|
+
"default": "./dist/api/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./types": {
|
|
39
|
+
"types": "./dist/types/index.d.ts",
|
|
40
|
+
"import": "./dist/types/index.js",
|
|
41
|
+
"default": "./dist/types/index.js"
|
|
27
42
|
}
|
|
28
43
|
},
|
|
29
44
|
"files": ["dist", "LICENSE", "README.md"],
|