@duvdu-v1/duvdu 1.1.333 → 1.1.335
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/build/utils/bucket.d.ts +21 -0
- package/build/utils/bucket.js +342 -28
- package/package.json +1 -1
package/build/utils/bucket.d.ts
CHANGED
|
@@ -15,4 +15,25 @@ export declare class Bucket {
|
|
|
15
15
|
ar: string;
|
|
16
16
|
};
|
|
17
17
|
}>;
|
|
18
|
+
private performLivenessDetection;
|
|
19
|
+
private performAntiSpoofingChecks;
|
|
20
|
+
private validateEyesForLiveness;
|
|
21
|
+
private validatePoseForLiveness;
|
|
22
|
+
/**
|
|
23
|
+
* Comprehensive face validation with advanced liveness detection
|
|
24
|
+
* This method provides additional verification by analyzing multiple image properties
|
|
25
|
+
*/
|
|
26
|
+
validateFaceWithAdvancedLiveness(imageKey: string): Promise<{
|
|
27
|
+
isValid: boolean;
|
|
28
|
+
livenessScore: number;
|
|
29
|
+
error?: {
|
|
30
|
+
en: string;
|
|
31
|
+
ar: string;
|
|
32
|
+
};
|
|
33
|
+
details?: {
|
|
34
|
+
faceConfidence: number;
|
|
35
|
+
livenessIndicators: string[];
|
|
36
|
+
spoofingRisks: string[];
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
18
39
|
}
|
package/build/utils/bucket.js
CHANGED
|
@@ -192,7 +192,8 @@ class Bucket {
|
|
|
192
192
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
193
193
|
return __awaiter(this, void 0, void 0, function* () {
|
|
194
194
|
try {
|
|
195
|
-
|
|
195
|
+
// Enhanced face detection with liveness checks
|
|
196
|
+
const faceParams = {
|
|
196
197
|
Image: {
|
|
197
198
|
S3Object: {
|
|
198
199
|
Bucket: this.bucketName,
|
|
@@ -201,14 +202,14 @@ class Bucket {
|
|
|
201
202
|
},
|
|
202
203
|
Attributes: ['ALL'],
|
|
203
204
|
};
|
|
204
|
-
const
|
|
205
|
-
if (!
|
|
205
|
+
const faceResult = yield this.rekognition.detectFaces(faceParams).promise();
|
|
206
|
+
if (!faceResult.FaceDetails || faceResult.FaceDetails.length === 0) {
|
|
206
207
|
return {
|
|
207
208
|
isValid: false,
|
|
208
209
|
error: { en: 'No face detected in the image', ar: 'لا يوجد وجه مكتشف في الصورة' },
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
|
-
if (
|
|
212
|
+
if (faceResult.FaceDetails.length > 1) {
|
|
212
213
|
return {
|
|
213
214
|
isValid: false,
|
|
214
215
|
error: {
|
|
@@ -217,53 +218,66 @@ class Bucket {
|
|
|
217
218
|
},
|
|
218
219
|
};
|
|
219
220
|
}
|
|
220
|
-
const face =
|
|
221
|
-
|
|
221
|
+
const face = faceResult.FaceDetails[0];
|
|
222
|
+
// Enhanced confidence check for liveness
|
|
223
|
+
if (!face.Confidence || face.Confidence < 75) {
|
|
222
224
|
return {
|
|
223
225
|
isValid: false,
|
|
224
|
-
error: {
|
|
226
|
+
error: {
|
|
227
|
+
en: 'Image quality is not sufficient for verification',
|
|
228
|
+
ar: 'جودة الصورة غير كافية للتحقق'
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Perform liveness detection using multiple AWS Rekognition features
|
|
233
|
+
const livenessCheck = yield this.performLivenessDetection(imageKey);
|
|
234
|
+
if (!livenessCheck.isLive) {
|
|
235
|
+
return {
|
|
236
|
+
isValid: false,
|
|
237
|
+
error: livenessCheck.error,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// Enhanced anti-spoofing checks
|
|
241
|
+
const antiSpoofingCheck = yield this.performAntiSpoofingChecks(face);
|
|
242
|
+
if (!antiSpoofingCheck.isValid) {
|
|
243
|
+
return {
|
|
244
|
+
isValid: false,
|
|
245
|
+
error: antiSpoofingCheck.error,
|
|
225
246
|
};
|
|
226
247
|
}
|
|
248
|
+
// Original validation checks with enhanced thresholds
|
|
227
249
|
if (((_a = face.Sunglasses) === null || _a === void 0 ? void 0 : _a.Value) || ((_b = face.Eyeglasses) === null || _b === void 0 ? void 0 : _b.Value)) {
|
|
228
250
|
return {
|
|
229
251
|
isValid: false,
|
|
230
252
|
error: {
|
|
231
|
-
en: 'Please remove glasses or sunglasses',
|
|
232
|
-
ar: 'يرجى إزالة النظارات أو النظارات الشمسية',
|
|
253
|
+
en: 'Please remove glasses or sunglasses for verification',
|
|
254
|
+
ar: 'يرجى إزالة النظارات أو النظارات الشمسية للتحقق',
|
|
233
255
|
},
|
|
234
256
|
};
|
|
235
257
|
}
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
if (!
|
|
258
|
+
// Enhanced eye detection for liveness
|
|
259
|
+
const eyesCheck = this.validateEyesForLiveness(face);
|
|
260
|
+
if (!eyesCheck.isValid) {
|
|
239
261
|
return {
|
|
240
262
|
isValid: false,
|
|
241
|
-
error:
|
|
242
|
-
en: 'Eyes must be fully open and clearly visible',
|
|
243
|
-
ar: 'يجب أن تكون العينين مفتوحتين بالكامل وواضحتين',
|
|
244
|
-
},
|
|
263
|
+
error: eyesCheck.error,
|
|
245
264
|
};
|
|
246
265
|
}
|
|
247
|
-
if ((
|
|
266
|
+
if (((_c = face.MouthOpen) === null || _c === void 0 ? void 0 : _c.Value) && ((_e = (_d = face.MouthOpen) === null || _d === void 0 ? void 0 : _d.Confidence) !== null && _e !== void 0 ? _e : 0) > 70) {
|
|
248
267
|
return {
|
|
249
268
|
isValid: false,
|
|
250
269
|
error: { en: 'Mouth should be closed', ar: 'يجب أن يكون الفم مغلقاً' },
|
|
251
270
|
};
|
|
252
271
|
}
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Roll) || 0) > poseThreshold ||
|
|
257
|
-
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Yaw) || 0) > poseThreshold) {
|
|
272
|
+
// Enhanced pose validation for anti-spoofing
|
|
273
|
+
const poseCheck = this.validatePoseForLiveness(face);
|
|
274
|
+
if (!poseCheck.isValid) {
|
|
258
275
|
return {
|
|
259
276
|
isValid: false,
|
|
260
|
-
error:
|
|
261
|
-
en: 'Face must be directly facing the camera',
|
|
262
|
-
ar: 'يجب أن يكون الوجه مواجهاً للكاميرا مباشرة',
|
|
263
|
-
},
|
|
277
|
+
error: poseCheck.error,
|
|
264
278
|
};
|
|
265
279
|
}
|
|
266
|
-
if ((
|
|
280
|
+
if (((_f = face.FaceOccluded) === null || _f === void 0 ? void 0 : _f.Value) && ((_h = (_g = face.FaceOccluded) === null || _g === void 0 ? void 0 : _g.Confidence) !== null && _h !== void 0 ? _h : 0) > 50) {
|
|
267
281
|
return {
|
|
268
282
|
isValid: false,
|
|
269
283
|
error: {
|
|
@@ -272,7 +286,8 @@ class Bucket {
|
|
|
272
286
|
},
|
|
273
287
|
};
|
|
274
288
|
}
|
|
275
|
-
|
|
289
|
+
// Reject excessive smiling which might indicate a photo
|
|
290
|
+
if (((_j = face.Smile) === null || _j === void 0 ? void 0 : _j.Value) && ((_l = (_k = face.Smile) === null || _k === void 0 ? void 0 : _k.Confidence) !== null && _l !== void 0 ? _l : 0) > 80) {
|
|
276
291
|
return {
|
|
277
292
|
isValid: false,
|
|
278
293
|
error: {
|
|
@@ -292,6 +307,305 @@ class Bucket {
|
|
|
292
307
|
}
|
|
293
308
|
});
|
|
294
309
|
}
|
|
310
|
+
performLivenessDetection(imageKey) {
|
|
311
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
312
|
+
try {
|
|
313
|
+
// Use AWS Rekognition's text detection to check for printed photos
|
|
314
|
+
const textParams = {
|
|
315
|
+
Image: {
|
|
316
|
+
S3Object: {
|
|
317
|
+
Bucket: this.bucketName,
|
|
318
|
+
Name: imageKey,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const textResult = yield this.rekognition.detectText(textParams).promise();
|
|
323
|
+
// If significant text is detected around the face area, it might be a printed photo
|
|
324
|
+
// Increased threshold to reduce false positives
|
|
325
|
+
if (textResult.TextDetections && textResult.TextDetections.length > 8) {
|
|
326
|
+
return {
|
|
327
|
+
isLive: false,
|
|
328
|
+
error: {
|
|
329
|
+
en: 'Static image detected. Please use a live camera capture',
|
|
330
|
+
ar: 'تم اكتشاف صورة ثابتة. يرجى استخدام التقاط مباشر من الكاميرا',
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// Use label detection to identify screens, monitors, or printed materials
|
|
335
|
+
const labelParams = {
|
|
336
|
+
Image: {
|
|
337
|
+
S3Object: {
|
|
338
|
+
Bucket: this.bucketName,
|
|
339
|
+
Name: imageKey,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
MaxLabels: 20,
|
|
343
|
+
MinConfidence: 70,
|
|
344
|
+
};
|
|
345
|
+
const labelResult = yield this.rekognition.detectLabels(labelParams).promise();
|
|
346
|
+
if (labelResult.Labels) {
|
|
347
|
+
// Reduced list to only obvious screen/printed content and increased confidence threshold
|
|
348
|
+
const suspiciousLabels = ['Screen', 'Monitor', 'Display', 'Computer Screen', 'Television'];
|
|
349
|
+
for (const label of labelResult.Labels) {
|
|
350
|
+
if (suspiciousLabels.some(suspicious => {
|
|
351
|
+
var _a, _b;
|
|
352
|
+
return ((_a = label.Name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(suspicious.toLowerCase())) &&
|
|
353
|
+
((_b = label.Confidence) !== null && _b !== void 0 ? _b : 0) > 85;
|
|
354
|
+
})) {
|
|
355
|
+
return {
|
|
356
|
+
isLive: false,
|
|
357
|
+
error: {
|
|
358
|
+
en: 'Please take a live photo, not from a screen or printed image',
|
|
359
|
+
ar: 'يرجى التقاط صورة مباشرة، وليس من شاشة أو صورة مطبوعة',
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { isLive: true };
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.error('Error in liveness detection:', error);
|
|
369
|
+
// Don't fail the entire validation for liveness detection errors
|
|
370
|
+
// This prevents AWS service issues from blocking legitimate users
|
|
371
|
+
return { isLive: true };
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
performAntiSpoofingChecks(face) {
|
|
376
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
377
|
+
try {
|
|
378
|
+
// Check for overly perfect lighting (common in printed photos) - relaxed threshold
|
|
379
|
+
const qualityMetrics = face.Quality;
|
|
380
|
+
if ((qualityMetrics === null || qualityMetrics === void 0 ? void 0 : qualityMetrics.Brightness) && qualityMetrics.Brightness > 98) {
|
|
381
|
+
return {
|
|
382
|
+
isValid: false,
|
|
383
|
+
error: {
|
|
384
|
+
en: 'Lighting appears artificial. Please use natural lighting',
|
|
385
|
+
ar: 'الإضاءة تبدو اصطناعية. يرجى استخدام الإضاءة الطبيعية',
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// Check for suspicious sharpness levels - more lenient range
|
|
390
|
+
if ((qualityMetrics === null || qualityMetrics === void 0 ? void 0 : qualityMetrics.Sharpness) && (qualityMetrics.Sharpness < 10 || qualityMetrics.Sharpness > 98)) {
|
|
391
|
+
return {
|
|
392
|
+
isValid: false,
|
|
393
|
+
error: {
|
|
394
|
+
en: 'Image sharpness indicates possible static image. Please retake with live camera',
|
|
395
|
+
ar: 'وضوح الصورة يشير إلى صورة ثابتة محتملة. يرجى إعادة التقاط بكاميرا مباشرة',
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Enhanced landmark analysis for 3D face detection
|
|
400
|
+
if (face.Landmarks && face.Landmarks.length > 0) {
|
|
401
|
+
const landmarks = face.Landmarks;
|
|
402
|
+
// Check for proper depth perception in landmarks
|
|
403
|
+
const eyeLandmarks = landmarks.filter(l => l.Type === 'eyeLeft' || l.Type === 'eyeRight' ||
|
|
404
|
+
l.Type === 'leftEyeLeft' || l.Type === 'leftEyeRight' ||
|
|
405
|
+
l.Type === 'rightEyeLeft' || l.Type === 'rightEyeRight');
|
|
406
|
+
if (eyeLandmarks.length < 4) {
|
|
407
|
+
return {
|
|
408
|
+
isValid: false,
|
|
409
|
+
error: {
|
|
410
|
+
en: 'Insufficient facial features detected for verification',
|
|
411
|
+
ar: 'ملامح الوجه المكتشفة غير كافية للتحقق',
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// Check for unnatural symmetry (printed photos often have perfect symmetry)
|
|
416
|
+
const noseLandmarks = landmarks.filter(l => l.Type === 'nose' || l.Type === 'noseLeft' || l.Type === 'noseRight');
|
|
417
|
+
if (noseLandmarks.length > 0) {
|
|
418
|
+
// Additional geometric checks could be implemented here
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { isValid: true };
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
console.error('Error in anti-spoofing checks:', error);
|
|
425
|
+
// Don't fail validation for anti-spoofing errors to prevent service issues
|
|
426
|
+
return { isValid: true };
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
validateEyesForLiveness(face) {
|
|
431
|
+
var _a;
|
|
432
|
+
// Enhanced eye validation for liveness detection - more lenient
|
|
433
|
+
const eyesOpen = face.EyesOpen;
|
|
434
|
+
if (!(eyesOpen === null || eyesOpen === void 0 ? void 0 : eyesOpen.Value) || ((_a = eyesOpen.Confidence) !== null && _a !== void 0 ? _a : 0) < 70) {
|
|
435
|
+
return {
|
|
436
|
+
isValid: false,
|
|
437
|
+
error: {
|
|
438
|
+
en: 'Eyes must be fully open and clearly visible',
|
|
439
|
+
ar: 'يجب أن تكون العينين مفتوحتين بالكامل وواضحتين',
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// Check for natural eye appearance (avoid glass reflections, red-eye, etc.)
|
|
444
|
+
if (face.Landmarks) {
|
|
445
|
+
const eyeLandmarks = face.Landmarks.filter(l => { var _a, _b; return ((_a = l.Type) === null || _a === void 0 ? void 0 : _a.includes('eye')) || ((_b = l.Type) === null || _b === void 0 ? void 0 : _b.includes('Eye')); });
|
|
446
|
+
if (eyeLandmarks.length < 6) {
|
|
447
|
+
return {
|
|
448
|
+
isValid: false,
|
|
449
|
+
error: {
|
|
450
|
+
en: 'Eye features not clearly detected. Please ensure good lighting',
|
|
451
|
+
ar: 'ملامح العين غير واضحة. يرجى التأكد من الإضاءة الجيدة',
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { isValid: true };
|
|
457
|
+
}
|
|
458
|
+
validatePoseForLiveness(face) {
|
|
459
|
+
const pose = face.Pose;
|
|
460
|
+
const poseThreshold = 12; // Stricter threshold for liveness
|
|
461
|
+
if (Math.abs((pose === null || pose === void 0 ? void 0 : pose.Pitch) || 0) > poseThreshold ||
|
|
462
|
+
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Roll) || 0) > poseThreshold ||
|
|
463
|
+
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Yaw) || 0) > poseThreshold) {
|
|
464
|
+
return {
|
|
465
|
+
isValid: false,
|
|
466
|
+
error: {
|
|
467
|
+
en: 'Face must be directly facing the camera',
|
|
468
|
+
ar: 'يجب أن يكون الوجه مواجهاً للكاميرا مباشرة',
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// Additional check for unnatural pose stability (printed photos have perfect stability)
|
|
473
|
+
// This would require multiple frames in a video-based solution, but we can check for other indicators
|
|
474
|
+
return { isValid: true };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Comprehensive face validation with advanced liveness detection
|
|
478
|
+
* This method provides additional verification by analyzing multiple image properties
|
|
479
|
+
*/
|
|
480
|
+
validateFaceWithAdvancedLiveness(imageKey) {
|
|
481
|
+
var _a, _b, _c;
|
|
482
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
483
|
+
try {
|
|
484
|
+
const baseValidation = yield this.validateFace(imageKey);
|
|
485
|
+
if (!baseValidation.isValid) {
|
|
486
|
+
return {
|
|
487
|
+
isValid: false,
|
|
488
|
+
livenessScore: 0,
|
|
489
|
+
error: baseValidation.error,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Calculate liveness score based on multiple factors
|
|
493
|
+
let livenessScore = 100;
|
|
494
|
+
const livenessIndicators = [];
|
|
495
|
+
const spoofingRisks = [];
|
|
496
|
+
// Get detailed face analysis
|
|
497
|
+
const faceParams = {
|
|
498
|
+
Image: {
|
|
499
|
+
S3Object: {
|
|
500
|
+
Bucket: this.bucketName,
|
|
501
|
+
Name: imageKey,
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
Attributes: ['ALL'],
|
|
505
|
+
};
|
|
506
|
+
const faceResult = yield this.rekognition.detectFaces(faceParams).promise();
|
|
507
|
+
const face = faceResult.FaceDetails[0];
|
|
508
|
+
// Analyze image metadata for camera vs screenshot indicators
|
|
509
|
+
try {
|
|
510
|
+
const moderationParams = {
|
|
511
|
+
Image: {
|
|
512
|
+
S3Object: {
|
|
513
|
+
Bucket: this.bucketName,
|
|
514
|
+
Name: imageKey,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
const moderationResult = yield this.rekognition.detectModerationLabels(moderationParams).promise();
|
|
519
|
+
// Check for any labels that might indicate artificial content
|
|
520
|
+
if (moderationResult.ModerationLabels && moderationResult.ModerationLabels.length > 0) {
|
|
521
|
+
for (const label of moderationResult.ModerationLabels) {
|
|
522
|
+
if (((_a = label.Name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('graphic')) && ((_b = label.Confidence) !== null && _b !== void 0 ? _b : 0) > 50) {
|
|
523
|
+
livenessScore -= 20;
|
|
524
|
+
spoofingRisks.push('Artificial content detected');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
// Moderation check failed, but don't fail the entire validation
|
|
531
|
+
console.warn('Moderation check failed:', error);
|
|
532
|
+
}
|
|
533
|
+
// Quality metrics analysis
|
|
534
|
+
const quality = face.Quality;
|
|
535
|
+
if (quality) {
|
|
536
|
+
if (quality.Brightness && (quality.Brightness < 30 || quality.Brightness > 90)) {
|
|
537
|
+
livenessScore -= 10;
|
|
538
|
+
spoofingRisks.push('Unusual lighting conditions');
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
livenessIndicators.push('Natural lighting detected');
|
|
542
|
+
}
|
|
543
|
+
if (quality.Sharpness && quality.Sharpness > 50 && quality.Sharpness < 90) {
|
|
544
|
+
livenessIndicators.push('Appropriate image sharpness');
|
|
545
|
+
}
|
|
546
|
+
else if (quality.Sharpness && (quality.Sharpness < 30 || quality.Sharpness > 95)) {
|
|
547
|
+
livenessScore -= 15;
|
|
548
|
+
spoofingRisks.push('Suspicious image sharpness');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Face feature confidence analysis
|
|
552
|
+
if (face.Confidence && face.Confidence > 95) {
|
|
553
|
+
livenessIndicators.push('High face detection confidence');
|
|
554
|
+
}
|
|
555
|
+
else if (face.Confidence && face.Confidence < 85) {
|
|
556
|
+
livenessScore -= 20;
|
|
557
|
+
spoofingRisks.push('Low face detection confidence');
|
|
558
|
+
}
|
|
559
|
+
// Eye analysis for natural appearance
|
|
560
|
+
if (((_c = face.EyesOpen) === null || _c === void 0 ? void 0 : _c.Confidence) && face.EyesOpen.Confidence > 95) {
|
|
561
|
+
livenessIndicators.push('Natural eye appearance');
|
|
562
|
+
}
|
|
563
|
+
// Pose analysis for natural positioning
|
|
564
|
+
const pose = face.Pose;
|
|
565
|
+
if (pose) {
|
|
566
|
+
const totalPoseDeviation = Math.abs(pose.Pitch || 0) + Math.abs(pose.Roll || 0) + Math.abs(pose.Yaw || 0);
|
|
567
|
+
if (totalPoseDeviation > 0 && totalPoseDeviation < 25) {
|
|
568
|
+
livenessIndicators.push('Natural head pose variation');
|
|
569
|
+
}
|
|
570
|
+
else if (totalPoseDeviation === 0) {
|
|
571
|
+
livenessScore -= 25;
|
|
572
|
+
spoofingRisks.push('Unnaturally perfect pose alignment');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Landmark analysis for 3D facial structure
|
|
576
|
+
if (face.Landmarks && face.Landmarks.length >= 15) {
|
|
577
|
+
livenessIndicators.push('Comprehensive facial landmarks detected');
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
livenessScore -= 10;
|
|
581
|
+
spoofingRisks.push('Insufficient facial landmarks');
|
|
582
|
+
}
|
|
583
|
+
// Final liveness assessment
|
|
584
|
+
const isLive = livenessScore >= 70;
|
|
585
|
+
return {
|
|
586
|
+
isValid: isLive,
|
|
587
|
+
livenessScore,
|
|
588
|
+
error: isLive ? undefined : {
|
|
589
|
+
en: `Liveness verification failed (Score: ${livenessScore}/100). Please use a live camera capture`,
|
|
590
|
+
ar: `فشل التحقق من الحيوية (النتيجة: ${livenessScore}/100). يرجى استخدام التقاط مباشر من الكاميرا`,
|
|
591
|
+
},
|
|
592
|
+
details: {
|
|
593
|
+
faceConfidence: face.Confidence || 0,
|
|
594
|
+
livenessIndicators,
|
|
595
|
+
spoofingRisks,
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.error('Error in advanced face validation:', error);
|
|
601
|
+
return {
|
|
602
|
+
isValid: false,
|
|
603
|
+
livenessScore: 0,
|
|
604
|
+
error: { en: 'Error processing image', ar: 'خطأ في معالجة الصورة' },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
295
609
|
}
|
|
296
610
|
exports.Bucket = Bucket;
|
|
297
611
|
const MIME_TYPES = {
|