@facesmash/sdk 0.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/dist/index.cjs ADDED
@@ -0,0 +1,658 @@
1
+ 'use strict';
2
+
3
+ var PocketBase = require('pocketbase');
4
+ var faceapi = require('@vladmandic/face-api');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var PocketBase__default = /*#__PURE__*/_interopDefault(PocketBase);
27
+ var faceapi__namespace = /*#__PURE__*/_interopNamespace(faceapi);
28
+
29
+ // src/core/client.ts
30
+ var modelsLoaded = false;
31
+ async function loadModels(config, onProgress) {
32
+ if (modelsLoaded) {
33
+ onProgress?.(100);
34
+ return true;
35
+ }
36
+ try {
37
+ try {
38
+ const tf2 = faceapi__namespace.tf;
39
+ if (tf2) {
40
+ await tf2.setBackend("webgl");
41
+ await tf2.ready();
42
+ if (tf2.env().flagRegistry?.CANVAS2D_WILL_READ_FREQUENTLY) {
43
+ tf2.env().set("CANVAS2D_WILL_READ_FREQUENTLY", true);
44
+ }
45
+ if (tf2.env().flagRegistry?.WEBGL_EXP_CONV) {
46
+ tf2.env().set("WEBGL_EXP_CONV", true);
47
+ }
48
+ if (config.debug) {
49
+ console.log(`[FaceSmash] TF.js backend: ${tf2.getBackend()}`);
50
+ }
51
+ }
52
+ } catch {
53
+ }
54
+ onProgress?.(10);
55
+ await Promise.all([
56
+ faceapi__namespace.nets.ssdMobilenetv1.loadFromUri(config.modelUrl),
57
+ faceapi__namespace.nets.tinyFaceDetector.loadFromUri(config.modelUrl),
58
+ faceapi__namespace.nets.faceLandmark68Net.loadFromUri(config.modelUrl),
59
+ faceapi__namespace.nets.faceRecognitionNet.loadFromUri(config.modelUrl),
60
+ faceapi__namespace.nets.faceExpressionNet.loadFromUri(config.modelUrl)
61
+ ]);
62
+ modelsLoaded = true;
63
+ onProgress?.(100);
64
+ if (config.debug) {
65
+ console.log("[FaceSmash] Models loaded successfully");
66
+ }
67
+ return true;
68
+ } catch (error) {
69
+ if (config.debug) {
70
+ console.error("[FaceSmash] Failed to load models:", error);
71
+ }
72
+ return false;
73
+ }
74
+ }
75
+ function areModelsLoaded() {
76
+ return modelsLoaded;
77
+ }
78
+ function getSsdOptions(minConfidence) {
79
+ return new faceapi__namespace.SsdMobilenetv1Options({ minConfidence });
80
+ }
81
+ function getTinyOptions() {
82
+ return new faceapi__namespace.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.4 });
83
+ }
84
+ async function extractDescriptor(input, config) {
85
+ try {
86
+ const media = typeof input === "string" ? await faceapi__namespace.fetchImage(input) : input;
87
+ let detection = await faceapi__namespace.detectSingleFace(media, getSsdOptions(config.minDetectionConfidence)).withFaceLandmarks().withFaceDescriptor();
88
+ if (!detection) {
89
+ detection = await faceapi__namespace.detectSingleFace(media, getTinyOptions()).withFaceLandmarks().withFaceDescriptor();
90
+ }
91
+ return detection?.descriptor ?? null;
92
+ } catch (error) {
93
+ if (config.debug) {
94
+ console.error("[FaceSmash] Descriptor extraction failed:", error);
95
+ }
96
+ return null;
97
+ }
98
+ }
99
+ async function analyzeFace(imageData, config) {
100
+ try {
101
+ const img = await faceapi__namespace.fetchImage(imageData);
102
+ let detection = await faceapi__namespace.detectSingleFace(img, getSsdOptions(config.minDetectionConfidence)).withFaceLandmarks().withFaceDescriptor();
103
+ if (!detection) {
104
+ detection = await faceapi__namespace.detectSingleFace(img, getTinyOptions()).withFaceLandmarks().withFaceDescriptor();
105
+ }
106
+ if (!detection) return null;
107
+ const headPose = estimateHeadPose(detection.landmarks, detection.detection.box);
108
+ const imgWidth = img.width || 640;
109
+ const imgHeight = img.height || 480;
110
+ const faceSizeCheck = validateFaceSize(detection.detection.box, imgWidth, imgHeight);
111
+ if (!faceSizeCheck.isValid) {
112
+ return {
113
+ descriptor: detection.descriptor,
114
+ normalizedDescriptor: normalizeDescriptor(detection.descriptor),
115
+ confidence: detection.detection.score,
116
+ qualityScore: 0,
117
+ lightingScore: 0,
118
+ headPose,
119
+ faceSizeCheck,
120
+ eyeAspectRatio: 0,
121
+ rejectionReason: faceSizeCheck.reason
122
+ };
123
+ }
124
+ const { avgEAR } = getEyeAspectRatios(detection.landmarks);
125
+ let lightingAnalysis;
126
+ try {
127
+ lightingAnalysis = analyzeLighting(detection, img);
128
+ } catch {
129
+ lightingAnalysis = {
130
+ score: 0.5,
131
+ brightness: 0.5,
132
+ contrast: 0.5,
133
+ evenness: 0.5,
134
+ conditions: { tooDark: false, tooBright: false, uneven: false, optimal: false }
135
+ };
136
+ }
137
+ let qualityScore = Math.min(detection.detection.score, 1);
138
+ qualityScore *= 0.7 + lightingAnalysis.score * 0.3;
139
+ const faceArea = detection.detection.box.width * detection.detection.box.height;
140
+ const imageArea = 640 * 640;
141
+ const sizeRatio = Math.min(faceArea / imageArea, 0.3) / 0.3;
142
+ qualityScore *= 0.8 + sizeRatio * 0.2;
143
+ if (!headPose.isFrontal) {
144
+ const anglePenalty = Math.max(0.5, 1 - (Math.abs(headPose.yaw) + Math.abs(headPose.pitch)) * 0.3);
145
+ qualityScore *= anglePenalty;
146
+ }
147
+ qualityScore = Math.max(0, Math.min(1, qualityScore));
148
+ return {
149
+ descriptor: detection.descriptor,
150
+ normalizedDescriptor: normalizeDescriptor(detection.descriptor),
151
+ confidence: detection.detection.score,
152
+ qualityScore,
153
+ lightingScore: lightingAnalysis.score,
154
+ headPose,
155
+ faceSizeCheck,
156
+ eyeAspectRatio: avgEAR
157
+ };
158
+ } catch (error) {
159
+ if (config.debug) {
160
+ console.error("[FaceSmash] Face analysis failed:", error);
161
+ }
162
+ return null;
163
+ }
164
+ }
165
+ async function processImages(images, config) {
166
+ if (images.length === 1) {
167
+ return extractDescriptor(images[0], config);
168
+ }
169
+ const descriptors = [];
170
+ for (const image of images) {
171
+ const d = await extractDescriptor(image, config);
172
+ if (d) descriptors.push(d);
173
+ }
174
+ if (descriptors.length === 0) return null;
175
+ const avg = new Float32Array(descriptors[0].length);
176
+ for (let i = 0; i < avg.length; i++) {
177
+ let sum = 0;
178
+ for (const d of descriptors) sum += d[i];
179
+ avg[i] = sum / descriptors.length;
180
+ }
181
+ return avg;
182
+ }
183
+ function euclidean(a, b) {
184
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
185
+ }
186
+ function calculateEAR(eye) {
187
+ if (eye.length < 6) return 0.3;
188
+ const v1 = euclidean(eye[1], eye[5]);
189
+ const v2 = euclidean(eye[2], eye[4]);
190
+ const h = euclidean(eye[0], eye[3]);
191
+ return h === 0 ? 0 : (v1 + v2) / (2 * h);
192
+ }
193
+ function getEyeAspectRatios(landmarks) {
194
+ const leftEAR = calculateEAR(landmarks.getLeftEye());
195
+ const rightEAR = calculateEAR(landmarks.getRightEye());
196
+ return { leftEAR, rightEAR, avgEAR: (leftEAR + rightEAR) / 2 };
197
+ }
198
+ function estimateHeadPose(landmarks, box) {
199
+ const nose = landmarks.getNose();
200
+ const jaw = landmarks.getJawOutline();
201
+ const noseTip = nose[3];
202
+ const faceCenterX = box.x + box.width / 2;
203
+ const faceCenterY = box.y + box.height / 2;
204
+ const yaw = (noseTip.x - faceCenterX) / (box.width / 2);
205
+ const pitch = (noseTip.y - faceCenterY) / (box.height / 2);
206
+ const jawLeft = jaw[0];
207
+ const jawRight = jaw[jaw.length - 1];
208
+ const roll = Math.atan2(jawRight.y - jawLeft.y, jawRight.x - jawLeft.x);
209
+ const isFrontal = Math.abs(yaw) < 0.35 && Math.abs(pitch) < 0.4 && Math.abs(roll) < 0.25;
210
+ return { yaw, pitch, roll, isFrontal };
211
+ }
212
+ function validateFaceSize(box, frameWidth = 640, frameHeight = 480) {
213
+ const ratio = box.width * box.height / (frameWidth * frameHeight);
214
+ if (ratio < 0.02) return { isValid: false, ratio, reason: "Face too far from camera" };
215
+ if (ratio > 0.65) return { isValid: false, ratio, reason: "Face too close to camera" };
216
+ if (box.width < 80 || box.height < 80) return { isValid: false, ratio, reason: "Face too small for reliable recognition" };
217
+ return { isValid: true, ratio };
218
+ }
219
+ function normalizeDescriptor(descriptor) {
220
+ let norm = 0;
221
+ for (let i = 0; i < descriptor.length; i++) norm += descriptor[i] ** 2;
222
+ norm = Math.sqrt(norm);
223
+ if (norm === 0) return descriptor;
224
+ const normalized = new Float32Array(descriptor.length);
225
+ for (let i = 0; i < descriptor.length; i++) normalized[i] = descriptor[i] / norm;
226
+ return normalized;
227
+ }
228
+ function analyzeLighting(detection, imageElement) {
229
+ const canvas = document.createElement("canvas");
230
+ const ctx = canvas.getContext("2d");
231
+ if (!ctx) throw new Error("Cannot get canvas context");
232
+ canvas.width = imageElement.width || 640;
233
+ canvas.height = imageElement.height || 640;
234
+ if (imageElement instanceof HTMLImageElement) {
235
+ ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);
236
+ } else {
237
+ ctx.drawImage(imageElement, 0, 0);
238
+ }
239
+ const faceBox = detection.detection.box;
240
+ const faceImageData = ctx.getImageData(
241
+ Math.max(0, faceBox.x - 20),
242
+ Math.max(0, faceBox.y - 20),
243
+ Math.min(canvas.width - faceBox.x, faceBox.width + 40),
244
+ Math.min(canvas.height - faceBox.y, faceBox.height + 40)
245
+ );
246
+ const pixels = faceImageData.data;
247
+ let totalBrightness = 0;
248
+ const brightnessValues = [];
249
+ for (let i = 0; i < pixels.length; i += 4) {
250
+ const brightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
251
+ totalBrightness += brightness;
252
+ brightnessValues.push(brightness);
253
+ }
254
+ const avgBrightness = totalBrightness / (pixels.length / 4);
255
+ const variance = brightnessValues.reduce((acc, val) => acc + (val - avgBrightness) ** 2, 0) / brightnessValues.length;
256
+ const contrast = Math.sqrt(variance);
257
+ const evenness = Math.max(0, 1 - contrast / 128);
258
+ const tooDark = avgBrightness < 80;
259
+ const tooBright = avgBrightness > 200;
260
+ const uneven = evenness < 0.6;
261
+ const optimal = !tooDark && !tooBright && !uneven;
262
+ let score = 0.5;
263
+ if (optimal) score = 0.9;
264
+ else if (tooDark) score = Math.max(0.2, avgBrightness / 160);
265
+ else if (tooBright) score = Math.max(0.2, (255 - avgBrightness) / 110);
266
+ else if (uneven) score = Math.max(0.3, evenness);
267
+ return {
268
+ score,
269
+ brightness: avgBrightness / 255,
270
+ contrast: Math.min(contrast / 64, 1),
271
+ evenness,
272
+ conditions: { tooDark, tooBright, uneven, optimal }
273
+ };
274
+ }
275
+ function calculateSimilarity(d1, d2) {
276
+ return 1 - faceapi__namespace.euclideanDistance(d1, d2);
277
+ }
278
+ function facesMatch(d1, d2, threshold = 0.45) {
279
+ return calculateSimilarity(d1, d2) >= threshold;
280
+ }
281
+ function enhancedMatch(descriptor1, descriptor2, baseThreshold = 0.45, confidenceBoost = 0, lightingScore = 0.5) {
282
+ if (descriptor1.length !== descriptor2.length) {
283
+ return { isMatch: false, similarity: 0, adaptedThreshold: baseThreshold };
284
+ }
285
+ const similarity = calculateSimilarity(descriptor1, descriptor2);
286
+ let adaptedThreshold = baseThreshold;
287
+ if (lightingScore < 0.4) {
288
+ adaptedThreshold = Math.max(0.35, adaptedThreshold - 0.05);
289
+ } else if (lightingScore > 0.8) {
290
+ adaptedThreshold = Math.min(0.6, adaptedThreshold + 0.02);
291
+ }
292
+ adaptedThreshold = Math.max(0.35, adaptedThreshold - confidenceBoost * 0.05);
293
+ return {
294
+ isMatch: similarity >= adaptedThreshold,
295
+ similarity,
296
+ adaptedThreshold
297
+ };
298
+ }
299
+ function multiTemplateMatch(newDescriptor, templates, baseThreshold, lightingScore = 0.5) {
300
+ if (templates.length === 0) {
301
+ return { isMatch: false, bestSimilarity: 0, avgSimilarity: 0, matchCount: 0 };
302
+ }
303
+ let bestSimilarity = 0;
304
+ let weightedSum = 0;
305
+ let totalWeight = 0;
306
+ let matchCount = 0;
307
+ for (const template of templates) {
308
+ if (!template.descriptor || template.descriptor.length === 0) continue;
309
+ const result = enhancedMatch(
310
+ newDescriptor,
311
+ template.descriptor,
312
+ baseThreshold,
313
+ template.weight,
314
+ lightingScore
315
+ );
316
+ if (result.similarity > bestSimilarity) {
317
+ bestSimilarity = result.similarity;
318
+ }
319
+ const w = template.quality * template.weight;
320
+ weightedSum += result.similarity * w;
321
+ totalWeight += w;
322
+ if (result.isMatch) matchCount++;
323
+ }
324
+ const avgSimilarity = totalWeight > 0 ? weightedSum / totalWeight : 0;
325
+ const isMatch = bestSimilarity >= baseThreshold || matchCount / templates.length >= 0.6;
326
+ return { isMatch, bestSimilarity, avgSimilarity, matchCount };
327
+ }
328
+ function calculateLearningWeight(qualityScore, lightingScore, confidence) {
329
+ let weight = 1;
330
+ if (qualityScore > 0.8) weight *= 1.5;
331
+ else if (qualityScore > 0.6) weight *= 1.2;
332
+ else if (qualityScore < 0.4) weight *= 0.5;
333
+ if (lightingScore > 0.7) weight *= 1.3;
334
+ else if (lightingScore < 0.4) weight *= 0.7;
335
+ if (confidence > 0.8) weight *= 1.2;
336
+ else if (confidence < 0.5) weight *= 0.8;
337
+ return Math.max(0.1, Math.min(weight, 3));
338
+ }
339
+
340
+ // src/core/types.ts
341
+ var DEFAULT_CONFIG = {
342
+ apiUrl: "https://api.facesmash.app",
343
+ modelUrl: "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model",
344
+ minDetectionConfidence: 0.3,
345
+ matchThreshold: 0.45,
346
+ minQualityScore: 0.2,
347
+ maxTemplatesPerUser: 10,
348
+ debug: false
349
+ };
350
+ function resolveConfig(config) {
351
+ return { ...DEFAULT_CONFIG, ...config };
352
+ }
353
+
354
+ // src/core/client.ts
355
+ var FaceSmashClient = class {
356
+ constructor(config) {
357
+ this.listeners = [];
358
+ this._modelsLoaded = false;
359
+ this.config = resolveConfig(config);
360
+ this.pb = new PocketBase__default.default(this.config.apiUrl);
361
+ this.pb.autoCancellation(false);
362
+ }
363
+ // ─── Event System ───────────────────────────────────────────
364
+ on(listener) {
365
+ this.listeners.push(listener);
366
+ return () => {
367
+ this.listeners = this.listeners.filter((l) => l !== listener);
368
+ };
369
+ }
370
+ emit(event) {
371
+ for (const listener of this.listeners) {
372
+ try {
373
+ listener(event);
374
+ } catch {
375
+ }
376
+ }
377
+ }
378
+ // ─── Model Loading ──────────────────────────────────────────
379
+ get isReady() {
380
+ return this._modelsLoaded;
381
+ }
382
+ async init(onProgress) {
383
+ if (this._modelsLoaded) return true;
384
+ this.emit({ type: "models-loading", progress: 0 });
385
+ const success = await loadModels(this.config, (progress) => {
386
+ onProgress?.(progress);
387
+ this.emit({ type: "models-loading", progress });
388
+ });
389
+ if (success) {
390
+ this._modelsLoaded = true;
391
+ this.emit({ type: "models-loaded" });
392
+ } else {
393
+ this.emit({ type: "models-error", error: "Failed to load face recognition models" });
394
+ }
395
+ return success;
396
+ }
397
+ // ─── Face Analysis ──────────────────────────────────────────
398
+ async analyzeFace(imageData) {
399
+ this.ensureReady();
400
+ const result = await analyzeFace(imageData, this.config);
401
+ if (result) {
402
+ this.emit({ type: "face-detected", analysis: result });
403
+ } else {
404
+ this.emit({ type: "face-lost" });
405
+ }
406
+ return result;
407
+ }
408
+ // ─── Login ──────────────────────────────────────────────────
409
+ async login(images) {
410
+ this.ensureReady();
411
+ this.emit({ type: "login-start" });
412
+ try {
413
+ let bestAnalysis = null;
414
+ for (const img of images) {
415
+ const analysis = await analyzeFace(img, this.config);
416
+ if (analysis && !analysis.rejectionReason) {
417
+ if (!bestAnalysis || analysis.qualityScore > bestAnalysis.qualityScore) {
418
+ bestAnalysis = analysis;
419
+ }
420
+ }
421
+ }
422
+ if (!bestAnalysis) {
423
+ const error2 = "No face detected in any image";
424
+ this.emit({ type: "login-failed", error: error2 });
425
+ return { success: false, error: error2 };
426
+ }
427
+ if (bestAnalysis.qualityScore < this.config.minQualityScore) {
428
+ const error2 = "Face quality too low. Improve lighting and face the camera directly.";
429
+ this.emit({ type: "login-failed", error: error2 });
430
+ return { success: false, error: error2 };
431
+ }
432
+ const profiles = await this.pb.collection("user_profiles").getFullList();
433
+ if (profiles.length === 0) {
434
+ const error2 = "No registered users found";
435
+ this.emit({ type: "login-failed", error: error2 });
436
+ return { success: false, error: error2 };
437
+ }
438
+ let bestMatch = { user: null, similarity: 0 };
439
+ for (const profile of profiles) {
440
+ if (!profile.face_embedding) continue;
441
+ const storedEmbedding = new Float32Array(profile.face_embedding);
442
+ let matchResult = enhancedMatch(
443
+ bestAnalysis.descriptor,
444
+ storedEmbedding,
445
+ this.config.matchThreshold,
446
+ 0,
447
+ bestAnalysis.lightingScore
448
+ );
449
+ try {
450
+ const templates = await this.pb.collection("face_templates").getList(1, 50, {
451
+ filter: `user_email="${profile.email}"`,
452
+ sort: "-quality_score"
453
+ });
454
+ if (templates.items.length > 0) {
455
+ const templateData = templates.items.filter((t) => t.descriptor && t.descriptor.length > 0).map((t) => ({
456
+ descriptor: new Float32Array(t.descriptor),
457
+ quality: t.quality_score || 0.5,
458
+ weight: 1
459
+ }));
460
+ if (templateData.length > 0) {
461
+ const multiResult = multiTemplateMatch(
462
+ bestAnalysis.descriptor,
463
+ templateData,
464
+ this.config.matchThreshold,
465
+ bestAnalysis.lightingScore
466
+ );
467
+ if (multiResult.bestSimilarity > matchResult.similarity) {
468
+ matchResult = {
469
+ isMatch: multiResult.isMatch,
470
+ similarity: multiResult.bestSimilarity,
471
+ adaptedThreshold: this.config.matchThreshold
472
+ };
473
+ }
474
+ }
475
+ }
476
+ } catch {
477
+ }
478
+ const userProfile = {
479
+ id: profile.id,
480
+ name: profile.name,
481
+ email: profile.email,
482
+ face_embedding: profile.face_embedding,
483
+ created: profile.created,
484
+ updated: profile.updated
485
+ };
486
+ if (matchResult.similarity > bestMatch.similarity) {
487
+ bestMatch = { user: userProfile, similarity: matchResult.similarity };
488
+ }
489
+ if (matchResult.isMatch) {
490
+ try {
491
+ await this.storeLoginScan(userProfile, bestAnalysis);
492
+ } catch {
493
+ }
494
+ this.emit({
495
+ type: "login-success",
496
+ user: userProfile,
497
+ similarity: matchResult.similarity
498
+ });
499
+ return { success: true, user: userProfile, similarity: matchResult.similarity };
500
+ }
501
+ }
502
+ const error = bestMatch.similarity > 0.4 ? "Face partially matched but did not meet security threshold." : "Face not recognized.";
503
+ this.emit({ type: "login-failed", error, bestSimilarity: bestMatch.similarity });
504
+ return { success: false, error, similarity: bestMatch.similarity };
505
+ } catch (err) {
506
+ const error = err instanceof Error ? err.message : "Unknown error during login";
507
+ this.emit({ type: "login-failed", error });
508
+ return { success: false, error };
509
+ }
510
+ }
511
+ // ─── Registration ───────────────────────────────────────────
512
+ async register(name, images, email) {
513
+ this.ensureReady();
514
+ this.emit({ type: "register-start" });
515
+ try {
516
+ let bestAnalysis = null;
517
+ let bestImageIdx = 0;
518
+ for (let i = 0; i < images.length; i++) {
519
+ const analysis = await analyzeFace(images[i], this.config);
520
+ if (analysis && !analysis.rejectionReason) {
521
+ if (!bestAnalysis || analysis.qualityScore > bestAnalysis.qualityScore) {
522
+ bestAnalysis = analysis;
523
+ bestImageIdx = i;
524
+ }
525
+ }
526
+ }
527
+ if (!bestAnalysis) {
528
+ const error = "No face detected in any image";
529
+ this.emit({ type: "register-failed", error });
530
+ return { success: false, error };
531
+ }
532
+ if (bestAnalysis.qualityScore < this.config.minQualityScore) {
533
+ const error = "Face quality too low for registration.";
534
+ this.emit({ type: "register-failed", error });
535
+ return { success: false, error };
536
+ }
537
+ const existingProfiles = await this.pb.collection("user_profiles").getFullList();
538
+ for (const profile of existingProfiles) {
539
+ if (!profile.face_embedding) continue;
540
+ const stored = new Float32Array(profile.face_embedding);
541
+ if (stored.length !== bestAnalysis.descriptor.length) continue;
542
+ const similarity = 1 - (await import('@vladmandic/face-api')).euclideanDistance(bestAnalysis.descriptor, stored);
543
+ if (similarity >= 0.75) {
544
+ const error = `This face is already registered to ${profile.name || profile.email}`;
545
+ this.emit({ type: "register-failed", error });
546
+ return { success: false, error };
547
+ }
548
+ }
549
+ const embeddingArray = Array.from(bestAnalysis.descriptor);
550
+ const record = await this.pb.collection("user_profiles").create({
551
+ name,
552
+ email: email || `${name.toLowerCase().replace(/\s+/g, ".")}@facesmash.app`,
553
+ face_embedding: embeddingArray
554
+ });
555
+ await this.pb.collection("face_templates").create({
556
+ user_email: record.email,
557
+ descriptor: embeddingArray,
558
+ quality_score: bestAnalysis.qualityScore,
559
+ label: "registration"
560
+ });
561
+ await this.pb.collection("face_scans").create({
562
+ user_email: record.email,
563
+ face_embedding: JSON.stringify(embeddingArray),
564
+ confidence: String(bestAnalysis.confidence),
565
+ scan_type: "registration",
566
+ quality_score: String(bestAnalysis.qualityScore)
567
+ });
568
+ const user = {
569
+ id: record.id,
570
+ name: record.name,
571
+ email: record.email,
572
+ face_embedding: embeddingArray,
573
+ created: record.created,
574
+ updated: record.updated
575
+ };
576
+ this.emit({ type: "register-success", user });
577
+ return { success: true, user };
578
+ } catch (err) {
579
+ const error = err instanceof Error ? err.message : "Unknown error during registration";
580
+ this.emit({ type: "register-failed", error });
581
+ return { success: false, error };
582
+ }
583
+ }
584
+ // ─── Helpers ────────────────────────────────────────────────
585
+ ensureReady() {
586
+ if (!areModelsLoaded()) {
587
+ throw new Error(
588
+ "FaceSmash models not loaded. Call client.init() first."
589
+ );
590
+ }
591
+ }
592
+ async storeLoginScan(user, analysis) {
593
+ const embeddingArray = Array.from(analysis.descriptor);
594
+ await this.pb.collection("sign_in_logs").create({
595
+ user_email: user.email,
596
+ success: true
597
+ });
598
+ await this.pb.collection("face_scans").create({
599
+ user_email: user.email,
600
+ face_embedding: JSON.stringify(embeddingArray),
601
+ confidence: String(analysis.confidence),
602
+ scan_type: "login",
603
+ quality_score: String(analysis.qualityScore)
604
+ });
605
+ if (analysis.qualityScore > 0.5) {
606
+ const weight = calculateLearningWeight(
607
+ analysis.qualityScore,
608
+ analysis.lightingScore,
609
+ analysis.confidence
610
+ );
611
+ const learningRate = Math.min(weight * 0.1, 0.3);
612
+ const current = new Float32Array(user.face_embedding);
613
+ const updated = new Float32Array(current.length);
614
+ for (let i = 0; i < current.length; i++) {
615
+ updated[i] = current[i] * (1 - learningRate) + analysis.descriptor[i] * learningRate;
616
+ }
617
+ await this.pb.collection("user_profiles").update(user.id, {
618
+ face_embedding: Array.from(updated)
619
+ });
620
+ }
621
+ if (analysis.qualityScore > 0.6) {
622
+ const existing = await this.pb.collection("face_templates").getList(1, 50, {
623
+ filter: `user_email="${user.email}"`,
624
+ sort: "quality_score"
625
+ });
626
+ if (existing.items.length >= this.config.maxTemplatesPerUser) {
627
+ await this.pb.collection("face_templates").delete(existing.items[0].id);
628
+ }
629
+ await this.pb.collection("face_templates").create({
630
+ user_email: user.email,
631
+ descriptor: embeddingArray,
632
+ quality_score: analysis.qualityScore,
633
+ label: "auto"
634
+ });
635
+ }
636
+ }
637
+ };
638
+
639
+ // src/index.ts
640
+ function createFaceSmash(config) {
641
+ return new FaceSmashClient(config);
642
+ }
643
+
644
+ exports.FaceSmashClient = FaceSmashClient;
645
+ exports.analyzeFace = analyzeFace;
646
+ exports.areModelsLoaded = areModelsLoaded;
647
+ exports.calculateLearningWeight = calculateLearningWeight;
648
+ exports.calculateSimilarity = calculateSimilarity;
649
+ exports.createFaceSmash = createFaceSmash;
650
+ exports.enhancedMatch = enhancedMatch;
651
+ exports.extractDescriptor = extractDescriptor;
652
+ exports.facesMatch = facesMatch;
653
+ exports.loadModels = loadModels;
654
+ exports.multiTemplateMatch = multiTemplateMatch;
655
+ exports.normalizeDescriptor = normalizeDescriptor;
656
+ exports.processImages = processImages;
657
+ //# sourceMappingURL=index.cjs.map
658
+ //# sourceMappingURL=index.cjs.map