UNPKG

apacuana-sdk-web

Version:

Apacuana SDK for Web

370 lines (362 loc) 14.5 kB
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" @click=${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; } @keyframes 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