You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
8.3 KiB
196 lines
8.3 KiB
/** @odoo-module **/
|
|
import { patch } from "@web/core/utils/patch";
|
|
const KioskConfirm = require("hr_attendance.kiosk_confirm")
|
|
const session = require('web.session');
|
|
var rpc = require('web.rpc');
|
|
const MODEL_URL = '/face_recognized_attendance_login/static/src/js/weights';
|
|
faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL);
|
|
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
|
|
faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
|
|
faceapi.nets.tinyFaceDetector.load(MODEL_URL);
|
|
faceapi.nets.faceLandmark68TinyNet.load(MODEL_URL);
|
|
faceapi.nets.faceExpressionNet.load(MODEL_URL);
|
|
faceapi.nets.ageGenderNet.load(MODEL_URL);
|
|
|
|
patch(KioskConfirm.prototype,'face_recognized_attendance_login.kiosk',{
|
|
events: {
|
|
"click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
|
"click .o_hr_attendance_sign_in_out_icon": _.debounce(async function () {
|
|
await this.startWebcam();
|
|
}, 200, true),
|
|
},
|
|
// -------To start the camera-------
|
|
async startWebcam() {
|
|
const video = this.el.querySelector('#video');
|
|
try {
|
|
const video = this.el.querySelector('#video');
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
throw new Error('getUserMedia is not supported in this browser.');
|
|
}
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
|
video.srcObject = stream;
|
|
|
|
video.onloadedmetadata = () => {
|
|
video.play();
|
|
};
|
|
this.faceRecognition(video);
|
|
} catch (error) {
|
|
console.error('An error occurred while accessing the camera:', error);
|
|
this.__parentedParent.notifications.add(
|
|
'Unable to access webcam. Please check your device permissions or use a supported browser.', {
|
|
title: 'Webcam Error',
|
|
type: 'danger',
|
|
sticky: true,
|
|
className: "p-4"
|
|
}
|
|
);
|
|
}
|
|
},
|
|
// -----To start the face recognition-----------
|
|
async faceRecognition(video) {
|
|
const labeledFaceDescriptors = await this.getLabeledFaceDescriptions(video);
|
|
if (!labeledFaceDescriptors) {
|
|
console.error('No labeled face descriptors available.');
|
|
this.stopWebcamAndDetection();
|
|
return;
|
|
}
|
|
if (!this.faceMatcher) {
|
|
const labeledFaceDescriptors = await this.getLabeledFaceDescriptions();
|
|
this.faceMatcher = new faceapi.FaceMatcher([labeledFaceDescriptors]);
|
|
if (labeledFaceDescriptors && labeledFaceDescriptors.descriptor) {
|
|
this.faceMatcher = new faceapi.FaceMatcher([labeledFaceDescriptors.descriptor]);
|
|
} else {
|
|
console.error("Could not get face descriptor from reference image");
|
|
this.__parentedParent.notification.add("Failed to initialize face recognition, Please upload a new, properly formatted image.", {
|
|
type: "danger",
|
|
title: "Image detection failed!",
|
|
});
|
|
this.stopRecognition(video);
|
|
return;
|
|
}
|
|
}
|
|
let attendanceMarked = false;
|
|
let notificationSent = false;
|
|
this.faceRecognitionInterval = setInterval(async () => {
|
|
try {
|
|
const detections = await faceapi
|
|
.detectAllFaces(video)
|
|
.withFaceLandmarks()
|
|
.withFaceDescriptors();
|
|
if (detections.length === 0) {
|
|
if (!notificationSent) {
|
|
this.__parentedParent.notifications.add(
|
|
'No face detected.', {
|
|
title: 'Detection Failed!',
|
|
type: 'danger',
|
|
sticky: false,
|
|
className: "p-4"
|
|
}
|
|
);
|
|
notificationSent = true;
|
|
}
|
|
this.stopWebcamAndDetection();
|
|
return;
|
|
}
|
|
detections.forEach((detection) => {
|
|
const match = this.faceMatcher.findBestMatch(detection.descriptor);
|
|
if (match._distance < 0.4 && !attendanceMarked) {
|
|
const modal = this.el.querySelector('#video');
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
attendanceMarked = true;
|
|
notificationSent = false;
|
|
this.markAttendance();
|
|
clearInterval(this.faceRecognitionInterval);
|
|
this.stopWebcamAndDetection();
|
|
}
|
|
});
|
|
if (!attendanceMarked && !notificationSent) {
|
|
this.__parentedParent.notifications.add(
|
|
'Face is not recognized.', {
|
|
title: 'No Match!',
|
|
type: 'danger',
|
|
sticky: false,
|
|
className: "p-4"
|
|
}
|
|
);
|
|
notificationSent = true;
|
|
this.stopWebcamAndDetection();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during face recognition:', error);
|
|
this.stopWebcamAndDetection();
|
|
}
|
|
}, 100);
|
|
},
|
|
// ---------Fetch labeled face descriptions (employee's face data)------
|
|
async getLabeledFaceDescriptions(video) {
|
|
const employee_image_base64 = await rpc.query({
|
|
model: 'hr.employee',
|
|
method: 'get_kiosk_image',
|
|
args: [this.employee_id]
|
|
});
|
|
if (employee_image_base64) {
|
|
const employee_image = new Image();
|
|
employee_image.src = "data:image/jpeg;base64," + employee_image_base64;
|
|
try {
|
|
const detections = await faceapi
|
|
.detectSingleFace(employee_image)
|
|
.withFaceLandmarks()
|
|
.withFaceExpressions()
|
|
.withFaceDescriptor();
|
|
if (!detections) {
|
|
console.error('No face detected in the image.');
|
|
this.__parentedParent.notifications.add(
|
|
'No face detected in the image.Please upload a new, properly formatted image in the profile.', {
|
|
title: 'Image detection failed!',
|
|
type: 'danger',
|
|
sticky: false,
|
|
className: "p-4"
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
return detections;
|
|
} catch (error) {
|
|
console.error('Error during face detection:', error);
|
|
}
|
|
} else {
|
|
console.error('No image data found for the employee.');
|
|
}
|
|
},
|
|
// ----------Function to stop webcam and face detection-----
|
|
stopWebcamAndDetection() {
|
|
const video = this.el.querySelector('#video');
|
|
if (video.srcObject) {
|
|
const stream = video.srcObject;
|
|
const tracks = stream.getTracks();
|
|
tracks.forEach(track => track.stop());
|
|
video.srcObject = null; //
|
|
}
|
|
if (this.faceRecognitionInterval) {
|
|
clearInterval(this.faceRecognitionInterval);
|
|
this.faceRecognitionInterval = null;
|
|
}
|
|
this.faceMatcher = null;
|
|
},
|
|
// ------------Redirecting to welcome/checkout page ----------------------------------
|
|
markAttendance() {
|
|
const self = this;
|
|
this._rpc({
|
|
model: 'hr.employee',
|
|
method: 'attendance_manual',
|
|
args: [[this.employee_id], 'hr_attendance.hr_attendance_action_my_attendances']
|
|
}).then((result) => {
|
|
if (result.action) {
|
|
self.do_action(result.action);
|
|
} else if (result.warning) {
|
|
self.do_warn(result.warning);
|
|
}
|
|
}).catch((error) => {
|
|
console.error('Error marking attendance:', error);
|
|
});
|
|
},
|
|
})
|
|
|
|
|