apacuana-sdk-web
Version:
Apacuana SDK for Web
370 lines (362 loc) • 14.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { FaceDetector, FilesetResolver, } from "@mediapipe/tasks-vision";
let ApacuanaLivenessDetector = class ApacuanaLivenessDetector extends LitElement {
constructor() {
super(...arguments);
this.sessionId = "";
this._feedbackMessage = "Cargando modelo de IA...";
this._isFaceInOval = false;
this._isCapturing = false;
this._showWarning = false;
this._isFlashing = false;
this._videoElement = null;
this._faceDetector = null;
this._animationFrameHandle = null;
this._captureTimeout = null;
// Definimos los índices de los puntos clave que nos interesan
this._KEYPOINTS = {
LEFT_EYE: 0,
RIGHT_EYE: 1,
NOSE_TIP: 2,
MOUTH_CENTER: 3,
};
}
async connectedCallback() {
super.connectedCallback();
await this._initializeFaceDetector();
await this._startCamera();
}
disconnectedCallback() {
var _a;
super.disconnectedCallback();
if (this._animationFrameHandle) {
cancelAnimationFrame(this._animationFrameHandle);
}
if (this._captureTimeout) {
clearTimeout(this._captureTimeout);
}
this._stopCamera();
(_a = this._faceDetector) === null || _a === void 0 ? void 0 : _a.close();
}
async _initializeFaceDetector() {
try {
const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm");
this._faceDetector = await FaceDetector.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite",
delegate: "GPU",
},
runningMode: "VIDEO",
// Habilitamos la detección de puntos clave
});
this._feedbackMessage = "Buscando rostro...";
}
catch (error) {
console.error("Error al inicializar el detector de rostros:", error);
this._emitError("No se pudo cargar el modelo de IA.");
}
}
async _runDetectionLoop() {
if (!this._faceDetector ||
!this._videoElement ||
this._videoElement.readyState < 2) {
this._animationFrameHandle = requestAnimationFrame(() => this._runDetectionLoop());
return;
}
const detections = this._faceDetector.detectForVideo(this._videoElement, performance.now());
this._processDetections(detections.detections);
this._animationFrameHandle = requestAnimationFrame(() => this._runDetectionLoop());
}
_processDetections(detections) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
if (this._isCapturing)
return;
if (detections && detections.length > 0) {
const face = detections[0];
// Un rostro necesita un cuadro delimitador y puntos clave para ser válido
if (!face.boundingBox || !face.keypoints || face.keypoints.length < 4) {
this._feedbackMessage = "Buscando rostro...";
this._resetScan();
return;
}
// Coordenadas normalizadas del video (0.0 a 1.0)
const videoWidth = (_b = (_a = this._videoElement) === null || _a === void 0 ? void 0 : _a.videoWidth) !== null && _b !== void 0 ? _b : 1;
const videoHeight = (_d = (_c = this._videoElement) === null || _c === void 0 ? void 0 : _c.videoHeight) !== null && _d !== void 0 ? _d : 1;
const faceCenterX = (face.boundingBox.originX + face.boundingBox.width / 2) / videoWidth;
const faceCenterY = (face.boundingBox.originY + face.boundingBox.height / 2) / videoHeight;
const faceWidth = face.boundingBox.width / videoWidth;
// Verificamos si los puntos clave principales son visibles con un umbral más flexible
const areKeypointsVisible = ((_f = (_e = face.keypoints[this._KEYPOINTS.LEFT_EYE]) === null || _e === void 0 ? void 0 : _e.score) !== null && _f !== void 0 ? _f : 0) > 0.3 &&
((_h = (_g = face.keypoints[this._KEYPOINTS.RIGHT_EYE]) === null || _g === void 0 ? void 0 : _g.score) !== null && _h !== void 0 ? _h : 0) > 0.3 &&
((_k = (_j = face.keypoints[this._KEYPOINTS.NOSE_TIP]) === null || _j === void 0 ? void 0 : _j.score) !== null && _k !== void 0 ? _k : 0) > 0.3 &&
((_m = (_l = face.keypoints[this._KEYPOINTS.MOUTH_CENTER]) === null || _l === void 0 ? void 0 : _l.score) !== null && _m !== void 0 ? _m : 0) > 0.3;
const isHorizontallyCentered = faceCenterX > 0.4 && faceCenterX < 0.6;
const isVerticallyCentered = faceCenterY > 0.4 && faceCenterY < 0.6;
const isLargeEnough = faceWidth > 0.2;
if (isHorizontallyCentered && isVerticallyCentered) {
if (isLargeEnough) {
if (areKeypointsVisible) {
this._isFaceInOval = true;
if (!this._isCapturing) {
this._isCapturing = true; // Bloqueamos el proceso
this._showWarning = true;
this._feedbackMessage =
"Advertencia: La pantalla parpadeará con luces de colores.";
// 1. Mostramos la advertencia por 3 segundos
this._captureTimeout = window.setTimeout(() => {
this._showWarning = false;
this._isFlashing = true;
this._feedbackMessage = "Manténgase quieto durante el escaneo.";
// 2. Iniciamos el flash y esperamos otros 3 segundos para capturar
this._captureTimeout = window.setTimeout(() => this._captureImage(), 3000);
}, 3000);
}
}
else {
this._feedbackMessage =
"Asegúrese de que su rostro esté completamente visible";
this._resetScan();
}
}
else {
this._feedbackMessage = "Acérquese un poco más";
this._resetScan();
}
}
else {
this._feedbackMessage = "Centre su rostro en el óvalo";
this._resetScan();
}
}
else {
this._feedbackMessage = "Buscando rostro...";
this._resetScan();
}
}
_resetScan() {
this._isFaceInOval = false;
this._isCapturing = false;
this._showWarning = false;
this._isFlashing = false;
if (this._captureTimeout) {
clearTimeout(this._captureTimeout);
this._captureTimeout = null;
}
}
_captureImage() {
if (!this._videoElement)
return;
this._isFlashing = false; // Detenemos la animación de flash
if (this._animationFrameHandle) {
cancelAnimationFrame(this._animationFrameHandle);
this._animationFrameHandle = null;
}
const canvas = document.createElement("canvas");
canvas.width = this._videoElement.videoWidth;
canvas.height = this._videoElement.videoHeight;
const context = canvas.getContext("2d");
if (context) {
// La imagen del video está espejada, la volteamos en el canvas para que se vea correcta
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.drawImage(this._videoElement, 0, 0, canvas.width, canvas.height);
const imageDataURL = canvas.toDataURL("image/png");
// Emitimos el evento con la imagen capturada
this.dispatchEvent(new CustomEvent("liveness-capture-success", {
detail: { image: imageDataURL },
bubbles: true,
composed: true,
}));
this._feedbackMessage = "¡Captura exitosa!";
// Cerramos el componente después de 2 segundos
setTimeout(() => this._handleClose(), 2000);
}
}
async _startCamera() {
var _a, _b;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user" },
audio: false,
});
await this.updateComplete;
this._videoElement = (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector("video")) !== null && _b !== void 0 ? _b : null;
if (this._videoElement) {
this._videoElement.srcObject = stream;
this._videoElement.onloadeddata = () => {
this._runDetectionLoop(); // Inicia el bucle solo cuando el video está listo
};
}
}
catch (error) {
console.error("Error al acceder a la cámara:", error);
this._emitError("No se pudo acceder a la cámara.");
}
}
_stopCamera() {
if (this._videoElement && this._videoElement.srcObject) {
const stream = this._videoElement.srcObject;
stream.getTracks().forEach((track) => track.stop());
this._videoElement.srcObject = null;
}
}
_handleClose() {
this.dispatchEvent(new CustomEvent("liveness-complete", {
detail: { success: true, message: "Simulación completada" },
bubbles: true,
composed: true,
}));
}
_emitError(message) {
this.dispatchEvent(new CustomEvent("liveness-error", {
detail: { message },
bubbles: true,
composed: true,
}));
}
render() {
const overlayClass = this._isFaceInOval ? "overlay face-in" : "overlay";
return html `
<div class="container">
<video autoplay muted playsinline></video>
<div class=${overlayClass}></div>
${this._isFlashing ? html `<div class="flash-overlay"></div>` : ""}
<button class="close-button" =${this._handleClose}>×</button>
<div class="instructions">
<h3>${this._feedbackMessage}</h3>
${this._showWarning
? html `<p style="font-size: 0.8rem;">
Puede causar molestias a personas con epilepsia fotosensible.
</p>`
: ""}
</div>
</div>
`;
}
};
ApacuanaLivenessDetector.styles = css `
:host {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1);
}
.overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* Hacemos el óvalo un poco más grande */
width: 400px;
height: 500px;
box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.8);
border-radius: 50%;
border: 3px solid white;
transition: border-color 0.3s ease;
}
.overlay.face-in {
border-color: #4caf50; /* Verde */
}
.flash-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1001; /* Encima del video pero debajo de los botones/texto */
animation: flash-colors 0.5s linear infinite;
pointer-events: none;
}
flash-colors {
0%,
100% {
background-color: rgba(255, 0, 0, 0.7);
} /* Rojo */
25% {
background-color: rgba(0, 255, 0, 0.7);
} /* Verde */
50% {
background-color: rgba(0, 0, 255, 0.7);
} /* Azul */
75% {
background-color: rgba(255, 255, 255, 0.7);
} /* Blanco */
}
.instructions {
position: absolute;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
color: white;
text-align: center;
background-color: rgba(0, 0, 0, 0.5);
padding: 1rem;
border-radius: 8px;
width: 80%;
max-width: 400px;
}
.close-button {
position: absolute;
top: 20px;
right: 20px;
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: white;
z-index: 10;
}
`;
__decorate([
property({ type: String })
], ApacuanaLivenessDetector.prototype, "sessionId", void 0);
__decorate([
state()
], ApacuanaLivenessDetector.prototype, "_feedbackMessage", void 0);
__decorate([
state()
], ApacuanaLivenessDetector.prototype, "_isFaceInOval", void 0);
__decorate([
state()
], ApacuanaLivenessDetector.prototype, "_isCapturing", void 0);
__decorate([
state()
], ApacuanaLivenessDetector.prototype, "_showWarning", void 0);
__decorate([
state()
], ApacuanaLivenessDetector.prototype, "_isFlashing", void 0);
ApacuanaLivenessDetector = __decorate([
customElement("apacuana-liveness-detector")
], ApacuanaLivenessDetector);
export { ApacuanaLivenessDetector };
//# sourceMappingURL=apacuana-liveness-detector.js.map