JAAK Stamps SDK

Stamps(@jaak.ai/stamps) es la versión 2 de document-detector, un componente web que permite la captura de identificaciones.

1. Objetivo, alcance y usuarios

El objetivo de este documento es proporcionar una guía completa para la integración y uso del WebComponent Jaak Stamps, un componente especializado en la captura automatizada de documentos de identidad mediante tecnología de visión por computadora.

Este documento abarca la implementación técnica del componente, configuración de propiedades, métodos disponibles, manejo de eventos, y las mejores prácticas para su integración en aplicaciones web modernas.

Dirigido a: Desarrolladores frontend con experiencia en integraciones de WebComponents, HTML5, JavaScript/TypeScript y APIs web.

Nivel requerido: Conocimientos intermedios en desarrollo web, manejo de WebComponents, acceso a cámaras mediante MediaStream API, y arquitecturas basadas en componentes.

Demo en Vivo

Antes de empezar con la implementación, puedes probar el componente en funcionamiento:

Funcionalidades Clave

  • Detección automática en tiempo real: Identifica documentos de identificación a través de la cámara del dispositivo.
  • Guía visual para posicionamiento: Ayuda al usuario a alinear el documento para una captura óptima.
  • Captura adaptativa: Se ajusta automáticamente si el documento tiene o no reverso.
  • Clasificación inteligente de documentos: Determina automáticamente si se requiere captura del reverso.
  • Salida de imágenes base64: Proporciona tanto la imagen completa del cuadro de video como un recorte preciso del documento para cada lado capturado.
  • Control de cámara avanzado: Selección de cámara, enfoque automático y control de linterna.
  • Responsivo: Compatible con dispositivos móviles y de escritorio.
  • Optimización automática: Mejora del rendimiento y gestión inteligente de recursos.
  • Telemetría integrada: Soporte para OpenTelemetry con trazas distribuidas y métricas.

2. Desarrollo

2.1 Prerrequisitos técnicos

a) Requisitos técnicos

RequisitoVersión/EspecificaciónObligatorioNotas
Navegador WebChrome 67+, Firefox 63+, Safari 12+, Edge 79+Soporte para WebComponents y MediaStream API
ProtocolHTTPSRequerido para acceso a cámara web
JavaScriptES2017+Soporte para async/await y módulos ES6
Memoria RAM4GB+ recomendadoNoPara mejor rendimiento con modelos de IA
Cámara webCualquier cámara USB/integradaCon resolución mínima 640x480

b) Credenciales y configuración de accesos

Requisitos de acceso:

  • Protocolo HTTPS: Obligatorio para acceso a MediaStream API
  • Permisos de cámara: El usuario debe otorgar permisos de acceso a la cámara
  • Conexión a internet: Necesaria para descarga de modelos de detección desde CDN
  • Almacenamiento local: Para persistencia de preferencias de cámara (opcional)
  • Licencia del SDK: Obligatoria para el funcionamiento del componente

c) Obtención de Licencia

Existen dos formas de obtener la licencia del SDK:

Opción A: Solicitud Directa

  • Formato: String alfanumérico único
  • Ejemplo: "ABC-123-XYZ-789"
  • Solicitar a: [email protected]

Opción B: Generación mediante API

Si no obtiene la licencia directamente del equipo JAAK, puede generarla mediante el siguiente proceso:

Paso 1: Obtener el Trace ID

Llamar al endpoint de inicio de flujo KYC:

POST /v1/kyc/session

De la respuesta, obtener los headers traceparent y x-trace-id:

traceparent: 00-cf143715d7a2d4ffc3ef122f62384844-6f7048446dffdb89-00
x-trace-id: 32922b9bf22570da5e9895fa592c6852

Paso 2: Validar la Licencia

Construir la licencia agregando el prefijo L al x-trace-id:

Lcf143715d7a2d4ffc3ef122f62384844

El campo obtenido se utilizará para autenticar los SDKs y el campo traceparent debe ser enviado en los headers de todos los llamados por API que se realicen.

2.2 Configuración del entorno

Paso 1. Instalación

a) Método principal de instalación
npm install @jaak.ai/[email protected]

O mediante CDN:

<script type="module" src="https://unpkg.com/@jaak.ai/[email protected]/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js"></script>
b) Configuración inicial
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Jaak Stamps Demo</title>
</head>
<body>
<jaak-stamps
  id="documentCapture"
  license="your-license-key-here"
  license-environment="prod"
  app-id="my-custom-app"
  debug="false"
  mask-size="90"
  alignment-tolerance="15"
  crop-margin="20"
  preferred-camera="auto"
  capture-delay="1500"
  enable-back-document-timer="false"
  back-document-timer-duration="20">
</jaak-stamps>

<script type="module" src="https://unpkg.com/@jaak.ai/[email protected]/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js"></script>
<script type="module">
  const jaakStamps = document.getElementById('documentCapture');

  // Escuchar cuando el componente esté listo
  jaakStamps.addEventListener('isReady', (event) => {
    console.log('Componente listo:', event.detail);
  });

  // Escuchar cuando se complete la captura
  jaakStamps.addEventListener('captureCompleted', (event) => {
    console.log('Captura completada:', event.detail);
  });
</script>
</body>
</html>

Paso 2. Configuración Avanzada

// Configuración avanzada con manejo de errores
const jaakStamps = document.getElementById('documentCapture');

// Precargar modelos antes de iniciar captura
jaakStamps.preloadModel().then(result => {
  if (result.success) {
    console.log('Modelos precargados exitosamente');
  } else {
    console.error('Error al precargar modelos:', result.error);
  }
});

// Configurar eventos
jaakStamps.addEventListener('isReady', handleReady);
jaakStamps.addEventListener('captureCompleted', handleCaptureCompleted);

function handleReady(event) {
  console.log('Componente inicializado:', event.detail);
  // Opcional: mostrar botón para iniciar captura
}

function handleCaptureCompleted(event) {
  const images = event.detail;
  console.log('Imágenes capturadas:', {
    front: images.front,
    back: images.back,
    metadata: images.metadata
  });
}

2.3 Guía de implementación

2.3.1 Implementación en Vanilla JavaScript

a) Ejemplo mínimo funcional:
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Captura de Documentos</title>
  <style>
    jaak-stamps {
      width: 100%;
      max-width: 600px;
      height: 400px;
      display: block;
      margin: 20px auto;
    }

    .controls {
      text-align: center;
      margin: 20px;
    }

    button {
      padding: 10px 20px;
      margin: 5px;
      font-size: 16px;
      cursor: pointer;
    }

    .results {
      max-width: 600px;
      margin: 20px auto;
      padding: 20px;
      border: 1px solid #ccc;
      border-radius: 5px;
    }
  </style>
</head>
<body>
<div class="controls">
  <button id="startBtn" onclick="startCapture()">Iniciar Captura</button>
  <button id="resetBtn" onclick="resetCapture()">Reiniciar</button>
  <button id="statusBtn" onclick="checkStatus()">Estado</button>
</div>

<jaak-stamps
  id="documentCapture"
  mask-size="85"
  alignment-tolerance="15"
  crop-margin="20"
  preferred-camera="auto"
  capture-delay="1500">
</jaak-stamps>

<div id="results" class="results" style="display: none;">
  <h3>Resultados de la Captura</h3>
  <div id="imageResults"></div>
</div>

<script type="module" src="https://unpkg.com/@jaak.ai/[email protected]/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js"></script>
<script type="module">

  const jaakStamps = document.getElementById('documentCapture');
  const resultsDiv = document.getElementById('results');
  const imageResults = document.getElementById('imageResults');

  // Funciones globales para los botones
  window.startCapture = async () => {
    try {
      await jaakStamps.startCapture();
      console.log('Captura iniciada');
    } catch (error) {
      console.error('Error al iniciar captura:', error);
    }
  };

  window.resetCapture = async () => {
    await jaakStamps.resetCapture();
    resultsDiv.style.display = 'none';
  };

  window.checkStatus = async () => {
    const status = await jaakStamps.getStatus();
    console.log('Estado actual:', status);
  };

  // Event listeners
  jaakStamps.addEventListener('captureCompleted', (event) => {
    const images = event.detail;
    displayResults(images);
  });

  function displayResults(images) {
    imageResults.innerHTML = `
                <p><strong>Proceso completado:</strong> ${images.metadata.processCompleted}</p>
                <p><strong>Total de imágenes:</strong> ${images.metadata.totalImages}</p>
                <p><strong>Reverso omitido:</strong> ${images.metadata.backCaptureSkipped || 'No'}</p>
                <p><strong>Timestamp:</strong> ${images.timestamp}</p>
            `;
    resultsDiv.style.display = 'block';
  }
</script>
</body>
</html>
b) Manejo de respuesta/resultado:
// Manejo completo de eventos y métodos
const jaakStamps = document.getElementById('documentCapture');

// 1. Verificar cuando el componente esté listo
jaakStamps.addEventListener('isReady', async (event) => {
  console.log('Componente listo:', event.detail);

  // Obtener información de cámaras disponibles
  const cameraInfo = await jaakStamps.getCameraInfo();
  console.log('Cámaras disponibles:', cameraInfo);
});

// 2. Manejar la finalización de captura
jaakStamps.addEventListener('captureCompleted', (event) => {
  const capturedData = event.detail;

  // Procesar imágenes capturadas
  if (capturedData.front.fullFrame) {
    console.log('Imagen frontal capturada');
    // Mostrar o procesar imagen frontal
    displayImage(capturedData.front.fullFrame, 'front-display');
  }

  if (capturedData.back.fullFrame) {
    console.log('Imagen trasera capturada');
    // Mostrar o procesar imagen trasera
    displayImage(capturedData.back.fullFrame, 'back-display');
  }

  // Enviar imágenes al servidor directamente desde el evento
  sendToServer(capturedData);
});

function displayImage(base64Data, elementId) {
  const imgElement = document.getElementById(elementId);
  if (imgElement) {
    imgElement.src = base64Data;
  }
}

function sendToServer(images) {
  fetch('/api/process-documents', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(images)
  })
    .then(response => response.json())
    .then(data => console.log('Respuesta del servidor:', data))
    .catch(error => console.error('Error:', error));
}

// 3. Ejemplo alternativo: usar getCapturedImages() por separado
// (útil cuando necesitas obtener las imágenes en otro momento)
async function getImagesLater() {
  try {
    // Verificar que el proceso esté completado
    const isCompleted = await jaakStamps.isProcessCompleted();
    if (isCompleted) {
      const images = await jaakStamps.getCapturedImages();
      console.log('Imágenes obtenidas posteriormente:', images);
      return images;
    } else {
      console.log('El proceso de captura aún no está completado');
    }
  } catch (error) {
    console.error('Error al obtener imágenes:', error);
  }
}

2.3.2 Implementación en Angular

a) Instalación y configuración:
# Instalar el componente
npm install @jaak.ai/[email protected]

# Instalar tipos para TypeScript (opcional)
npm install --save-dev @types/node
b) Configuración del módulo:
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

// Importar el componente
import { defineCustomElements } from '@jaak.ai/stamps/loader';
defineCustomElements();

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA] // Permitir elementos personalizados
})
export class AppModule { }
c) Componente TypeScript:
// document-capture.component.ts
import { Component, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'app-document-capture',
  templateUrl: './document-capture.component.html',
  styleUrls: ['./document-capture.component.css']
})
export class DocumentCaptureComponent {
  @ViewChild('jaakStamps', { static: false }) jaakStamps!: ElementRef;

  isReady = false;
  isCapturing = false;
  capturedImages: any = null;

  onComponentReady(event: any) {
    this.isReady = event.detail;
    console.log('Componente listo:', this.isReady);
  }

  onCaptureCompleted(event: any) {
    this.capturedImages = event.detail;
    this.isCapturing = false;
    console.log('Captura completada:', this.capturedImages);
  }

  async startCapture() {
    if (!this.isReady) return;

    try {
      this.isCapturing = true;
      await this.jaakStamps.nativeElement.startCapture();
    } catch (error) {
      console.error('Error al iniciar captura:', error);
      this.isCapturing = false;
    }
  }

  async resetCapture() {
    try {
      await this.jaakStamps.nativeElement.resetCapture();
      this.capturedImages = null;
      this.isCapturing = false;
    } catch (error) {
      console.error('Error al reiniciar captura:', error);
    }
  }
}
d) Template HTML:
<!-- document-capture.component.html -->
<div class="capture-container">
  <h2>Captura de Documento</h2>

  <div class="controls">
    <button
      (click)="startCapture()"
      [disabled]="!isReady || isCapturing"
      class="btn btn-primary">
      {{ isCapturing ? 'Capturando...' : 'Iniciar Captura' }}
    </button>
    <button
      (click)="resetCapture()"
      [disabled]="!isReady"
      class="btn btn-secondary">
      Reiniciar
    </button>
  </div>

  <jaak-stamps
    #jaakStamps
    mask-size="90"
    alignment-tolerance="15"
    crop-margin="20"
    preferred-camera="auto"
    capture-delay="1500"
    (isReady)="onComponentReady($event)"
    (captureCompleted)="onCaptureCompleted($event)"
    class="stamps-component">
  </jaak-stamps>

  <div *ngIf="capturedImages" class="results">
    <h3>Resultados de Captura</h3>
    <div class="metadata">
      <p><strong>Total de imágenes:</strong> {{ capturedImages.metadata.totalImages }}</p>
      <p><strong>Proceso completado:</strong> {{ capturedImages.metadata.processCompleted ? 'Sí' : 'No' }}</p>
    </div>
    <div class="images" *ngIf="capturedImages.front.fullFrame">
      <h4>Imagen Frontal:</h4>
      <img [src]="capturedImages.front.fullFrame" alt="Documento frente" class="captured-image">
    </div>
    <div class="images" *ngIf="capturedImages.back.fullFrame">
      <h4>Imagen Trasera:</h4>
      <img [src]="capturedImages.back.fullFrame" alt="Documento reverso" class="captured-image">
    </div>
  </div>
</div>
e) Estilos CSS:
/* document-capture.component.css */
.capture-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.controls {
  text-align: center;
  margin: 20px 0;
}

.btn {
  padding: 10px 20px;
  margin: 0 10px;
  font-size: 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.stamps-component {
  width: 100%;
  height: 400px;
  display: block;
  margin: 20px 0;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.results {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #f8f9fa;
}

.metadata p {
  margin: 5px 0;
}

.images {
  margin: 15px 0;
}

.captured-image {
  max-width: 100%;
  height: auto;
  border: 1px solid #ccc;
  border-radius: 4px;
}

2.3.3 Implementación en React

a) Instalación y configuración:
# Instalar el componente
npm install @jaak.ai/[email protected]

# Para TypeScript, instalar tipos
npm install --save-dev @types/react @types/react-dom
b) Configuración inicial:
// App.js - Importación y configuración básica
import React, { useRef, useEffect, useState } from 'react';

// Importar el componente
import { defineCustomElements } from '@jaak.ai/stamps/loader';
defineCustomElements();

function DocumentCapture() {
  const jaakStampsRef = useRef(null);
  const [isReady, setIsReady] = useState(false);
  const [isCapturing, setIsCapturing] = useState(false);
  const [capturedImages, setCapturedImages] = useState(null);

  useEffect(() => {
    const jaakStamps = jaakStampsRef.current;
    if (!jaakStamps) return;

    // Configurar eventos
    const handleReady = (event) => {
      setIsReady(event.detail);
      console.log('Componente listo:', event.detail);
    };

    const handleCaptureCompleted = (event) => {
      setCapturedImages(event.detail);
      setIsCapturing(false);
      console.log('Captura completada:', event.detail);
    };

    jaakStamps.addEventListener('isReady', handleReady);
    jaakStamps.addEventListener('captureCompleted', handleCaptureCompleted);

    // Cleanup
    return () => {
      jaakStamps.removeEventListener('isReady', handleReady);
      jaakStamps.removeEventListener('captureCompleted', handleCaptureCompleted);
    };
  }, []);

  const startCapture = async () => {
    if (!jaakStampsRef.current || !isReady) return;

    try {
      setIsCapturing(true);
      await jaakStampsRef.current.startCapture();
    } catch (error) {
      console.error('Error al iniciar captura:', error);
      setIsCapturing(false);
    }
  };

  const resetCapture = async () => {
    if (!jaakStampsRef.current) return;

    try {
      await jaakStampsRef.current.resetCapture();
      setCapturedImages(null);
      setIsCapturing(false);
    } catch (error) {
      console.error('Error al reiniciar:', error);
    }
  };

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h2>Captura de Documento</h2>

      <div style={{ textAlign: 'center', marginBottom: '20px' }}>
        <button
          onClick={startCapture}
          disabled={!isReady || isCapturing}
          style={{
            padding: '10px 20px',
            margin: '0 10px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: isReady && !isCapturing ? 'pointer' : 'not-allowed',
            opacity: isReady && !isCapturing ? 1 : 0.6
          }}
        >
          {isCapturing ? 'Capturando...' : 'Iniciar Captura'}
        </button>

        <button
          onClick={resetCapture}
          disabled={!isReady}
          style={{
            padding: '10px 20px',
            margin: '0 10px',
            backgroundColor: '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: isReady ? 'pointer' : 'not-allowed',
            opacity: isReady ? 1 : 0.6
          }}
        >
          Reiniciar
        </button>
      </div>

      <jaak-stamps
        ref={jaakStampsRef}
        mask-size="90"
        alignment-tolerance="15"
        crop-margin="20"
        preferred-camera="auto"
        capture-delay="1500"
        style={{
          width: '100%',
          height: '400px',
          display: 'block',
          border: '1px solid #ddd',
          borderRadius: '8px'
        }}
      />

      {capturedImages && (
        <div style={{
          marginTop: '20px',
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>Resultados de Captura</h3>
          <p><strong>Total de imágenes:</strong> {capturedImages.metadata.totalImages}</p>
          <p><strong>Proceso completado:</strong> {capturedImages.metadata.processCompleted ? 'Sí' : 'No'}</p>

          {capturedImages.front.fullFrame && (
            <div style={{ margin: '15px 0' }}>
              <h4>Imagen Frontal:</h4>
              <img
                src={capturedImages.front.fullFrame}
                alt="Documento frente"
                style={{
                  maxWidth: '100%',
                  height: 'auto',
                  border: '1px solid #ccc',
                  borderRadius: '4px'
                }}
              />
            </div>
          )}

          {capturedImages.back.fullFrame && (
            <div style={{ margin: '15px 0' }}>
              <h4>Imagen Trasera:</h4>
              <img
                src={capturedImages.back.fullFrame}
                alt="Documento reverso"
                style={{
                  maxWidth: '100%',
                  height: 'auto',
                  border: '1px solid #ccc',
                  borderRadius: '4px'
                }}
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default DocumentCapture;
c) Ejemplo con CSS separado:
// DocumentCapture.jsx
import React, { useRef, useEffect, useState } from 'react';
import { defineCustomElements } from '@jaak.ai/stamps/loader';
import './DocumentCapture.css';

defineCustomElements();

function DocumentCapture() {
  const jaakStampsRef = useRef(null);
  const [isReady, setIsReady] = useState(false);
  const [isCapturing, setIsCapturing] = useState(false);
  const [capturedImages, setCapturedImages] = useState(null);

  useEffect(() => {
    const jaakStamps = jaakStampsRef.current;
    if (!jaakStamps) return;

    const handleReady = (event) => {
      setIsReady(event.detail);
      console.log('Componente listo:', event.detail);
    };

    const handleCaptureCompleted = (event) => {
      setCapturedImages(event.detail);
      setIsCapturing(false);
      console.log('Captura completada:', event.detail);
    };

    jaakStamps.addEventListener('isReady', handleReady);
    jaakStamps.addEventListener('captureCompleted', handleCaptureCompleted);

    return () => {
      jaakStamps.removeEventListener('isReady', handleReady);
      jaakStamps.removeEventListener('captureCompleted', handleCaptureCompleted);
    };
  }, []);

  const startCapture = async () => {
    if (!jaakStampsRef.current || !isReady) return;

    try {
      setIsCapturing(true);
      await jaakStampsRef.current.startCapture();
    } catch (error) {
      console.error('Error al iniciar captura:', error);
      setIsCapturing(false);
    }
  };

  const resetCapture = async () => {
    if (!jaakStampsRef.current) return;

    try {
      await jaakStampsRef.current.resetCapture();
      setCapturedImages(null);
      setIsCapturing(false);
    } catch (error) {
      console.error('Error al reiniciar:', error);
    }
  };

  return (
    <div className="document-capture">
      <h2>Captura de Documento</h2>

      <div className="controls">
        <button
          onClick={startCapture}
          disabled={!isReady || isCapturing}
          className="btn btn-primary"
        >
          {isCapturing ? 'Capturando...' : 'Iniciar Captura'}
        </button>

        <button
          onClick={resetCapture}
          disabled={!isReady}
          className="btn btn-secondary"
        >
          Reiniciar
        </button>
      </div>

      <jaak-stamps
        ref={jaakStampsRef}
        mask-size="90"
        alignment-tolerance="15"
        crop-margin="20"
        preferred-camera="auto"
        capture-delay="1500"
        className="stamps-component"
      />

      {capturedImages && (
        <div className="results">
          <h3>Resultados de Captura</h3>
          <div className="metadata">
            <p><strong>Total de imágenes:</strong> {capturedImages.metadata.totalImages}</p>
            <p><strong>Proceso completado:</strong> {capturedImages.metadata.processCompleted ? 'Sí' : 'No'}</p>
          </div>

          {capturedImages.front.fullFrame && (
            <div className="image-section">
              <h4>Imagen Frontal:</h4>
              <img
                src={capturedImages.front.fullFrame}
                alt="Documento frente"
                className="captured-image"
              />
            </div>
          )}

          {capturedImages.back.fullFrame && (
            <div className="image-section">
              <h4>Imagen Trasera:</h4>
              <img
                src={capturedImages.back.fullFrame}
                alt="Documento reverso"
                className="captured-image"
              />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default DocumentCapture;
/* DocumentCapture.css */
.document-capture {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.controls {
  text-align: center;
  margin-bottom: 20px;
}

.btn {
  padding: 10px 20px;
  margin: 0 10px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.stamps-component {
  width: 100%;
  height: 400px;
  display: block;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.results {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #f8f9fa;
}

.metadata p {
  margin: 5px 0;
}

.image-section {
  margin: 15px 0;
}

.captured-image {
  max-width: 100%;
  height: auto;
  border: 1px solid #ccc;
  border-radius: 4px;
}

2.3.4 Implementación avanzada

a) Ejemplo completo funcional:
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Implementación Avanzada - Jaak Stamps</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f5f5f5;
    }

    .container {
      background-color: white;
      border-radius: 10px;
      padding: 30px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }

    .header {
      text-align: center;
      margin-bottom: 30px;
    }

    .controls {
      display: flex;
      justify-content: center;
      gap: 15px;
      margin-bottom: 30px;
    }

    .btn {
      padding: 12px 24px;
      font-size: 16px;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.3s ease;
    }

    .btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .btn-primary {
      background-color: #007bff;
      color: white;
    }

    .btn-primary:hover:not(:disabled) {
      background-color: #0056b3;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-secondary:hover:not(:disabled) {
      background-color: #545b62;
    }

    .btn-warning {
      background-color: #ffc107;
      color: #212529;
    }

    .btn-warning:hover:not(:disabled) {
      background-color: #e0a800;
    }

    .stamps-container {
      width: 100%;
      height: 500px;
      border: 2px solid #ddd;
      border-radius: 10px;
      overflow: hidden;
      margin-bottom: 30px;
    }

    jaak-stamps {
      width: 100%;
      height: 100%;
      display: block;
    }

    .status-panel {
      background-color: #f8f9fa;
      border: 1px solid #dee2e6;
      border-radius: 8px;
      padding: 20px;
      margin-bottom: 20px;
    }

    .status-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 15px;
    }

    .status-item {
      background-color: white;
      padding: 15px;
      border-radius: 6px;
      border: 1px solid #e9ecef;
    }

    .status-label {
      font-weight: bold;
      color: #6c757d;
      font-size: 14px;
      margin-bottom: 5px;
    }

    .status-value {
      font-size: 16px;
      color: #333;
    }

    .status-ready {
      color: #28a745;
    }

    .status-error {
      color: #dc3545;
    }

    .results-section {
      background-color: #e8f5e8;
      border: 1px solid #c3e6cb;
      border-radius: 8px;
      padding: 20px;
      margin-top: 20px;
    }

    .results-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
    }

    .image-preview {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 20px;
      margin-top: 20px;
    }

    .image-card {
      background-color: white;
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 15px;
      text-align: center;
    }

    .image-card img {
      max-width: 100%;
      height: auto;
      border-radius: 4px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }

    .error-message {
      background-color: #f8d7da;
      border: 1px solid #f5c6cb;
      border-radius: 6px;
      padding: 15px;
      margin: 20px 0;
      color: #721c24;
    }

    .camera-selector {
      margin-bottom: 20px;
    }

    .camera-selector select {
      padding: 8px 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
    }
  </style>
</head>
<body>
<div class="container">
  <div class="header">
    <h1>Implementación Avanzada - Jaak Stamps</h1>
    <p>Ejemplo completo con control total del flujo y manejo de errores</p>
  </div>

  <div class="camera-selector" id="cameraSelector" style="display: none;">
    <label for="cameraSelect">Seleccionar cámara:</label>
    <select id="cameraSelect"></select>
  </div>

  <div class="controls">
    <button id="preloadBtn" class="btn btn-secondary">Precargar Modelos</button>
    <button id="startBtn" class="btn btn-primary" disabled>Iniciar Captura</button>
    <button id="resetBtn" class="btn btn-warning" disabled>Reiniciar</button>
    <button id="statusBtn" class="btn btn-secondary">Estado</button>
  </div>

  <div class="stamps-container">
    <jaak-stamps
      id="documentCapture"
      mask-size="90"
      alignment-tolerance="15"
      crop-margin="20"
      preferred-camera="auto"
      use-document-classification="true"
      capture-delay="1500"
      debug="false">
    </jaak-stamps>
  </div>

  <div class="status-panel">
    <h3>Estado del Componente</h3>
    <div class="status-grid">
      <div class="status-item">
        <div class="status-label">Estado General</div>
        <div class="status-value" id="generalStatus">Inicializando...</div>
      </div>
      <div class="status-item">
        <div class="status-label">Modelos Cargados</div>
        <div class="status-value" id="modelStatus">No</div>
      </div>
      <div class="status-item">
        <div class="status-label">Cámara Activa</div>
        <div class="status-value" id="cameraStatus">No</div>
      </div>
      <div class="status-item">
        <div class="status-label">Paso de Captura</div>
        <div class="status-value" id="captureStep">-</div>
      </div>
    </div>
  </div>

  <div id="errorContainer"></div>
  <div id="resultsContainer"></div>
</div>

<script type="module" src="https://unpkg.com/@jaak.ai/[email protected]/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js"></script>
<script type="module">
  // Implementación avanzada con control total del flujo
  class DocumentCaptureManager {
    constructor(elementId) {
      this.jaakStamps = document.getElementById(elementId);
      this.setupEventListeners();
      this.setupUI();
      this.isReady = false;
      this.currentError = null;
    }

    setupUI() {
      // Referencias a elementos del DOM
      this.preloadBtn = document.getElementById('preloadBtn');
      this.startBtn = document.getElementById('startBtn');
      this.resetBtn = document.getElementById('resetBtn');
      this.statusBtn = document.getElementById('statusBtn');
      this.cameraSelect = document.getElementById('cameraSelect');
      this.cameraSelector = document.getElementById('cameraSelector');
      this.errorContainer = document.getElementById('errorContainer');
      this.resultsContainer = document.getElementById('resultsContainer');

      // Event listeners para botones
      this.preloadBtn.addEventListener('click', () => this.preloadModels());
      this.startBtn.addEventListener('click', () => this.startCaptureProcess());
      this.resetBtn.addEventListener('click', () => this.resetCapture());
      this.statusBtn.addEventListener('click', () => this.showStatus());
      this.cameraSelect.addEventListener('change', (e) => this.switchCamera(e.target.value));
    }

    async preloadModels() {
      try {
        this.updateStatus('Precargando modelos...');
        this.preloadBtn.disabled = true;

        const result = await this.jaakStamps.preloadModel();
        if (result.success) {
          console.log('Modelos precargados exitosamente');
          this.updateStatus('Modelos cargados', 'ready');
          document.getElementById('modelStatus').textContent = 'Sí';
          this.onModelsReady();
        } else {
          console.error('Error al precargar modelos:', result.error);
          this.handleError('MODEL_LOAD_ERROR', result.error);
        }
      } catch (error) {
        console.error('Error en precarga de modelos:', error);
        this.handleError('MODEL_LOAD_EXCEPTION', error);
      } finally {
        this.preloadBtn.disabled = false;
      }
    }

    setupEventListeners() {
      this.jaakStamps.addEventListener('isReady', (event) => {
        this.onComponentReady(event.detail);
      });

      this.jaakStamps.addEventListener('captureCompleted', (event) => {
        this.onCaptureCompleted(event.detail);
      });
    }

    async onComponentReady(isReady) {
      this.isReady = isReady;
      if (isReady) {
        this.updateStatus('Componente listo', 'ready');

        // Configurar cámara preferida si es necesario
        await this.configureCameraSettings();

        // Habilitar interfaz de usuario
        this.enableUserInterface();
      } else {
        this.updateStatus('Error en inicialización', 'error');
      }
    }

    async configureCameraSettings() {
      try {
        const cameraInfo = await this.jaakStamps.getCameraInfo();

        if (cameraInfo.isMultipleCamerasAvailable) {
          this.showCameraSelector(cameraInfo.availableCameras);
        }

        // Configurar cámara preferida basada en tipo de dispositivo
        if (cameraInfo.deviceType === 'mobile') {
          await this.jaakStamps.setPreferredCamera('back');
        }

        document.getElementById('cameraStatus').textContent =
          cameraInfo.selectedCameraId ? 'Sí' : 'No';
      } catch (error) {
        console.error('Error al configurar cámara:', error);
        this.handleError('CAMERA_CONFIG_ERROR', error);
      }
    }

    async startCaptureProcess() {
      try {
        this.clearError();
        const status = await this.jaakStamps.getStatus();

        if (!status.isModelPreloaded) {
          throw new Error('Los modelos no están cargados. Precargar primero.');
        }

        this.updateStatus('Iniciando captura...');
        this.startBtn.disabled = true;

        await this.jaakStamps.startCapture();
        this.onCaptureStarted();
      } catch (error) {
        console.error('Error al iniciar captura:', error);
        this.handleError('CAPTURE_START_ERROR', error);
        this.startBtn.disabled = false;
      }
    }

    async onCaptureCompleted(capturedData) {
      try {
        this.updateStatus('Procesando imágenes...');

        // Validar imágenes capturadas
        if (!this.validateCapturedImages(capturedData)) {
          throw new Error('Imágenes capturadas no son válidas');
        }

        // Procesar imágenes
        const processedData = await this.processImages(capturedData);

        // Mostrar resultados
        this.displayResults(processedData);

        // Opcional: Enviar al servidor
        // const serverResponse = await this.uploadToServer(processedData);

        this.updateStatus('Captura completada', 'ready');
        this.onProcessingComplete(processedData);
      } catch (error) {
        console.error('Error en procesamiento:', error);
        this.handleError('PROCESSING_ERROR', error);
      }
    }

    validateCapturedImages(data) {
      return data.metadata.processCompleted &&
        data.front.fullFrame &&
        (data.back.fullFrame || data.metadata.backCaptureSkipped);
    }

    async processImages(data) {
      // Aplicar procesamiento adicional si es necesario
      return {
        ...data,
        processedAt: new Date().toISOString(),
        clientId: this.getClientId(),
        sessionId: this.getSessionId()
      };
    }

    async uploadToServer(data) {
      const response = await fetch('/api/document-verification', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.getAuthToken()}`
        },
        body: JSON.stringify(data)
      });

      if (!response.ok) {
        throw new Error(`Server error: ${response.status}`);
      }

      return response.json();
    }

    async resetCapture() {
      try {
        await this.jaakStamps.resetCapture();
        this.clearResults();
        this.clearError();
        this.updateStatus('Reiniciado', 'ready');
        this.startBtn.disabled = false;
        document.getElementById('captureStep').textContent = '-';
      } catch (error) {
        console.error('Error al reiniciar:', error);
        this.handleError('RESET_ERROR', error);
      }
    }

    async showStatus() {
      try {
        const status = await this.jaakStamps.getStatus();
        const cameraInfo = await this.jaakStamps.getCameraInfo();

        document.getElementById('generalStatus').textContent =
          this.isReady ? 'Listo' : 'No listo';
        document.getElementById('modelStatus').textContent =
          status.isModelPreloaded ? 'Sí' : 'No';
        document.getElementById('cameraStatus').textContent =
          status.isVideoActive ? 'Sí' : 'No';
        document.getElementById('captureStep').textContent =
          status.captureStep || '-';

        console.log('Estado completo:', { status, cameraInfo });
      } catch (error) {
        console.error('Error al obtener estado:', error);
      }
    }

    async switchCamera(cameraId) {
      try {
        if (cameraId) {
          await this.jaakStamps.setPreferredCamera(cameraId);
          this.updateStatus('Cámara cambiada');
        }
      } catch (error) {
        console.error('Error al cambiar cámara:', error);
        this.handleError('CAMERA_SWITCH_ERROR', error);
      }
    }

    showCameraSelector(cameras) {
      this.cameraSelect.innerHTML = '';
      cameras.forEach(camera => {
        const option = document.createElement('option');
        option.value = camera.id;
        option.textContent = camera.label || `Cámara ${camera.id}`;
        option.selected = camera.selected;
        this.cameraSelect.appendChild(option);
      });
      this.cameraSelector.style.display = 'block';
    }

    displayResults(data) {
      this.resultsContainer.innerHTML = `
                    <div class="results-section">
                        <div class="results-header">
                            <h3>Resultados de Captura</h3>
                            <small>Completado: ${data.processedAt}</small>
                        </div>
                        <div class="status-grid">
                            <div class="status-item">
                                <div class="status-label">Total de Imágenes</div>
                                <div class="status-value">${data.metadata.totalImages}</div>
                            </div>
                            <div class="status-item">
                                <div class="status-label">Proceso Completado</div>
                                <div class="status-value">${data.metadata.processCompleted ? 'Sí' : 'No'}</div>
                            </div>
                            <div class="status-item">
                                <div class="status-label">Reverso Omitido</div>
                                <div class="status-value">${data.metadata.backCaptureSkipped ? 'Sí' : 'No'}</div>
                            </div>
                            <div class="status-item">
                                <div class="status-label">ID de Sesión</div>
                                <div class="status-value">${data.sessionId}</div>
                            </div>
                        </div>
                        <div class="image-preview">
                            ${data.front.fullFrame ? `
                                <div class="image-card">
                                    <h4>Imagen Frontal</h4>
                                    <img src="${data.front.fullFrame}" alt="Documento frente">
                                </div>
                            ` : ''}
                            ${data.back.fullFrame ? `
                                <div class="image-card">
                                    <h4>Imagen Trasera</h4>
                                    <img src="${data.back.fullFrame}" alt="Documento reverso">
                                </div>
                            ` : ''}
                        </div>
                    </div>
                `;
    }

    handleError(errorType, error) {
      this.currentError = { type: errorType, message: error.message || error };
      console.error(`${errorType}:`, error);

      this.showErrorMessage(errorType, error.message || error);
      this.updateStatus('Error', 'error');

      // Reportar error a servicio de monitoreo
      this.reportError(errorType, error);
    }

    showErrorMessage(type, message) {
      this.errorContainer.innerHTML = `
                    <div class="error-message">
                        <strong>Error (${type}):</strong> ${message}
                        <button onclick="document.getElementById('errorContainer').innerHTML = ''"
                                style="float: right; background: none; border: none; font-size: 18px; cursor: pointer;">×</button>
                    </div>
                `;
    }

    clearError() {
      this.errorContainer.innerHTML = '';
      this.currentError = null;
    }

    clearResults() {
      this.resultsContainer.innerHTML = '';
    }

    updateStatus(message, type = 'info') {
      const statusElement = document.getElementById('generalStatus');
      statusElement.textContent = message;
      statusElement.className = type === 'ready' ? 'status-ready' :
        type === 'error' ? 'status-error' : '';
    }

    // Métodos auxiliares
    onModelsReady() {
      console.log('Modelos listos para usar');
    }

    enableUserInterface() {
      this.startBtn.disabled = false;
      this.resetBtn.disabled = false;
      console.log('Interfaz de usuario habilitada');
    }

    onCaptureStarted() {
      console.log('Captura iniciada exitosamente');
      this.updateStatus('Capturando documento...');
    }

    onProcessingComplete(response) {
      console.log('Procesamiento completado:', response);
      this.startBtn.disabled = false;
    }

    reportError(type, error) {
      // Implementar envío a servicio de monitoreo
      console.log('Reportando error:', { type, error, timestamp: new Date().toISOString() });
    }

    getClientId() {
      return localStorage.getItem('clientId') || 'client-' + Date.now();
    }

    getSessionId() {
      return 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
    }

    getAuthToken() {
      return localStorage.getItem('authToken') || 'demo-token';
    }
  }

  // Inicializar el manager cuando el DOM esté listo
  document.addEventListener('DOMContentLoaded', () => {
    window.captureManager = new DocumentCaptureManager('documentCapture');
  });
</script>
</body>
</html>
b) Funcionalidades incluidas en el ejemplo avanzado:
  • Interfaz completa: Botones para precargar modelos, iniciar captura, reiniciar y consultar estado
  • Selector de cámara: Cambio dinámico entre cámaras disponibles
  • Panel de estado: Monitoreo en tiempo real del estado del componente
  • Manejo de errores: Captura y visualización de errores con tipos específicos
  • Visualización de resultados: Muestra las imágenes capturadas y metadata
  • Estilos completos: CSS moderno y responsive
  • Validación de datos: Verificación de imágenes capturadas antes del procesamiento
  • Gestión de sesiones: IDs únicos para cliente y sesión
  • Integración con servidor: Estructura preparada para envío a APIs backend

2.4 Referencias/Métodos

2.4.1 Especificación principal

WebComponent - jaak-stamps

Descripción: El componente jaak-stamps es un WebComponent especializado en la captura automatizada de documentos de identidad. Utiliza tecnología de visión por computadora con modelos de inteligencia artificial para detectar, alinear y capturar documentos de forma precisa. Está optimizado para funcionar en dispositivos móviles y de escritorio, con detección automática de capacidades del dispositivo.

2.4.2 Propiedades de entrada

Propiedades de Licencia (Requeridas)
PropiedadTipoRequeridoDescripciónEjemploValor por defecto
licensestringClave de licencia para autenticación del componentelicense="your-key"undefined
license-environmentstringNoAmbiente para validación de licencia: 'dev', 'qa', 'sandbox', 'prod'license-environment="prod"'prod'
app-idstringNoIdentificador de aplicación para validación y análisisapp-id="my-app"'jaak-stamps-web'
trace-idstringNoID de traza opcional para tracking distribuido (se auto-genera si no se proporciona)trace-id="abc123"undefined
Propiedades de Configuración Base
PropiedadTipoRequeridoDescripciónEjemploValor por defecto
debugbooleanNoActiva el modo debug con métricas de rendimiento y cajas de detección visiblesdebug="true"false
alignment-tolerancenumberNoTolerancia en píxeles para la detección de alineación del documentoalignment-tolerance="15"15
mask-sizenumberNoTamaño de la máscara de captura como porcentaje (50-100)mask-size="85"90
crop-marginnumberNoMargen adicional para el recorte de imagen en píxeles (0-100)crop-margin="5"20
use-document-classificationbooleanNoHabilita la clasificación automática de tipos de documentouse-document-classification="true"false
use-document-detectorbooleanNoActiva/desactiva el modelo de detección automática de documentosuse-document-detector="true"true
preferred-camerastringNoPreferencia de cámara a usar: 'auto', 'front', 'back'preferred-camera="back"'auto'
capture-delaynumberNoTiempo de espera en milisegundos antes de capturar después de detectar alineación (0-10000)capture-delay="2000"1500
enable-back-document-timerbooleanNoHabilita el temporizador automático para saltar la captura del reversoenable-back-document-timer="true"false
back-document-timer-durationnumberNoDuración en segundos del temporizador para saltar reverso automáticamenteback-document-timer-duration="30"20

Nota: El modelo implementado en la clasificación de documentos (use-document-classification) aún se encuentra en fase Beta por lo que podría presentar fallas en la exactitud de la detección.

Propiedades de Telemetría y OpenTelemetry
PropiedadTipoRequeridoDescripciónEjemploValor por defecto
telemetry-collector-urlstringNoURL del recolector OTLP para trazas distribuidastelemetry-collector-url="https://..."'https://collector.jaak.ai/v1/traces'
metrics-collector-urlstringNoURL del recolector OTLP para métricasmetrics-collector-url="https://..."'https://collector.jaak.ai/v1/metrics'
enable-telemetrybooleanNoHabilita envío de trazas distribuidas a OpenTelemetryenable-telemetry="true"true
enable-metricsbooleanNoHabilita envío de métricas a OpenTelemetryenable-metrics="true"true
metrics-export-interval-millisnumberNoIntervalo de exportación de métricas en milisegundosmetrics-export-interval-millis="60000"60000
propagate-trace-header-cors-urlsstringNoURLs (separadas por coma) para propagar headers W3C Trace Contextpropagate-trace-header-cors-urls="https://api.example.com"undefined
Propiedades de Contexto
PropiedadTipoRequeridoDescripciónEjemploValor por defecto
customer-idstringNoID del cliente para telemetría y análisiscustomer-id="customer123"undefined
tenant-idstringNoID del tenant para multi-tenancytenant-id="tenant456"undefined
environmentstringNoAmbiente de ejecución: 'development', 'staging', 'production'environment="production"'production'

2.4.3 Métodos públicos

MétodoParámetrosRetornoDescripción
startCapture()-Promise<void>Inicia el proceso de captura de documentos
stopCapture()-Promise<void>Detiene el proceso de captura actual y finaliza la sesión
resetCapture()-Promise<void>Reinicia el proceso de captura al estado inicial
skipBackCapture()-Promise<void>Omite la captura del reverso del documento
preloadModel()-Promise<{success: boolean, message?: string, error?: string}>Precarga los modelos de detección y clasificación
getCapturedImages()-Promise<CapturedImages>Obtiene las imágenes capturadas (solo disponible después de completar)
getStatus()-Promise<ComponentStatus>Obtiene el estado actual del componente
getCameraInfo()-Promise<CameraInfo>Obtiene información sobre las cámaras disponibles
setPreferredCamera(camera)camera: 'auto' | 'front' | 'back'Promise<{success: boolean, selectedCamera: string | null, availableCameras: number}>Configura la cámara preferida
isProcessCompleted()-Promise<boolean>Verifica si el proceso de captura ha sido completado
setCaptureDelay(delay)delay: numberPromise<{success: boolean, captureDelay: number}>Configura el tiempo de espera antes de capturar (0-10000ms)
getCaptureDelay()-Promise<number>Obtiene el tiempo de espera configurado para la captura

Tipos de datos de retorno:

interface CapturedImages {
  front: {
    fullFrame: string | null;  // Imagen completa en base64
    cropped: string | null;    // Imagen recortada en base64
  };
  back: {
    fullFrame: string | null;
    cropped: string | null;
  };
  metadata: {
    totalImages: number;
    processCompleted: boolean;
    backCaptureSkipped: boolean;
  };
  timestamp?: string;
}

interface ComponentStatus {
  isVideoActive: boolean;
  captureStep: 'front' | 'back' | 'completed';
  hasImages: boolean;
  isProcessCompleted: boolean;
  isModelPreloaded: boolean;
  componentStatus?: 'initializing' | 'loading' | 'ready' | 'error';
  componentMessage?: string;
}

interface CameraInfo {
  availableCameras: Array<{id: string, label: string, selected: boolean}>;
  selectedCameraId: string | null;
  deviceType: 'mobile' | 'desktop';
  isMultipleCamerasAvailable: boolean;
  preferredFacing: 'environment' | 'user' | null;
}

2.4.4 Eventos

EventoTipo de datoDescripción
isReadyCustomEvent<boolean>Se dispara cuando el componente está completamente inicializado y listo para usar
captureCompletedCustomEvent<CapturedImages>Se dispara cuando se completa el proceso de captura con las imágenes resultantes
traceIdGeneratedCustomEvent<{traceId: string}>Se dispara después de la validación de licencia con el ID de traza generado o proporcionado para tracking de peticiones

Ejemplo de manejo de eventos:

const jaakStamps = document.getElementById('documentCapture');

jaakStamps.addEventListener('isReady', (event) => {
  console.log('Componente listo:', event.detail); // boolean
  if (event.detail) {
    // El componente está listo para usar
    enableCaptureButton();
  }
});

jaakStamps.addEventListener('captureCompleted', (event) => {
  const images = event.detail; // CapturedImages
  console.log('Captura completada:', images);

  // Procesar imágenes capturadas
  processDocumentImages(images);
});

jaakStamps.addEventListener('traceIdGenerated', (event) => {
  const traceId = event.detail.traceId;
  console.log('Trace ID generado:', traceId);
  // Usar para correlacionar peticiones al backend
});

2.5 Telemetría y Observabilidad (OpenTelemetry)

El WebComponent Jaak Stamps incluye integración completa con OpenTelemetry para proporcionar observabilidad, trazabilidad y monitoreo de rendimiento en producción.

2.5.1 Características de Telemetría

Trazas Distribuidas (Distributed Tracing)

El componente implementa trazado distribuido siguiendo el estándar W3C Trace Context, permitiendo:

  • Seguimiento completo del flujo de captura de documentos
  • Correlación de peticiones entre frontend y backend
  • Identificación de cuellos de botella y problemas de rendimiento
  • Propagación de contexto mediante headers traceparent

Spans registrados automáticamente:

  • component.initialize - Inicialización del componente
  • capture.start - Inicio del proceso de captura
  • model.preload - Precarga de modelos IA
  • model.detection.load - Carga del modelo de detección
  • model.classification.load - Carga del modelo de clasificación

Métricas de Rendimiento

El componente exporta métricas detalladas a OpenTelemetry:

Contadores (Counters):

  • capture.counter - Número de capturas completadas
  • error.counter - Conteo de errores por tipo
  • model.load.counter - Conteo de cargas de modelo
  • user.interaction.counter - Interacciones del usuario

Histogramas (Latencias):

  • capture.latency - Tiempo de captura completo
  • model.load.latency - Tiempo de carga de modelos
  • detection.latency - Tiempo de detección por frame
  • image.size - Tamaño de imágenes capturadas

Gauges (Estado Actual):

  • active.sessions - Sesiones activas
  • memory.usage - Uso de memoria

2.5.2 Configuración de Telemetría

<jaak-stamps
  license="your-license-key"
  enable-telemetry="true"
  enable-metrics="true"
  telemetry-collector-url="https://collector.jaak.ai/v1/traces"
  metrics-collector-url="https://collector.jaak.ai/v1/metrics"
  metrics-export-interval-millis="60000"
  propagate-trace-header-cors-urls="https://api.example.com,https://backend.example.com"
  customer-id="customer123"
  tenant-id="tenant456"
  environment="production"
  trace-id="optional-custom-trace-id">
</jaak-stamps>

2.5.3 Uso del Trace ID

El componente genera automáticamente un ID de traza único para cada sesión, o puede recibir uno personalizado:

const jaakStamps = document.getElementById('documentCapture');

// Escuchar el evento traceIdGenerated
jaakStamps.addEventListener('traceIdGenerated', (event) => {
  const traceId = event.detail.traceId;
  console.log('Trace ID para esta sesión:', traceId);

  // Usar este traceId para correlacionar peticiones en tu backend
  fetch('/api/process-document', {
    method: 'POST',
    headers: {
      'X-Trace-Id': traceId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ /* datos */ })
  });
});

2.5.4 Atributos Enriquecidos

Las trazas y métricas incluyen automáticamente:

  • User Agent: Navegador, versión, sistema operativo
  • Geolocalización: País, región, ciudad (cuando está disponible)
  • Device Memory: Capacidad de memoria del dispositivo
  • Customer Context: customerId, tenantId, environment
  • Service Info: Nombre del servicio, versión del componente

2.5.5 Propagación de Contexto (CORS)

Para que las trazas se propaguen a tus servicios backend:

<jaak-stamps
  propagate-trace-header-cors-urls="https://api.example.com,https://services.example.com">
</jaak-stamps>

Esto configurará automáticamente los interceptores de fetch y XMLHttpRequest para incluir el header traceparent en las peticiones a esas URLs.

2.5.6 Visualización con Jaeger/Zipkin

Las trazas exportadas son compatibles con:

  • Jaeger - Sistema de trazado distribuido
  • Zipkin - Sistema alternativo de trazado
  • Grafana Tempo - Backend de trazas de Grafana
  • Cualquier backend compatible con OTLP (OpenTelemetry Protocol)

2.6 Validación de Licencia

El componente requiere una licencia válida para funcionar. La validación se realiza automáticamente al cargar el componente.

2.6.1 Configuración de Licencia

<jaak-stamps
  license="your-license-key-here"
  license-environment="prod"
  app-id="my-application">
</jaak-stamps>

2.6.2 Ambientes Disponibles

AmbienteURL de ValidaciónUso
devhttps://api.dev.jaak.aiDesarrollo local
qahttps://api.qa.jaak.aiPruebas de calidad
sandboxhttps://api.sandbox.jaak.aiPruebas de integración
prodhttps://services.api.jaak.aiProducción

2.6.3 Manejo de Errores de Licencia

Si la licencia no es válida, el componente mostrará un error y no funcionará:

const jaakStamps = document.getElementById('documentCapture');

jaakStamps.addEventListener('isReady', (event) => {
  if (!event.detail) {
    console.error('Error: Licencia inválida o componente no inicializado');
  }
});

Mensajes de error comunes:

  • "License key is required" - No se proporcionó licencia
  • "Invalid license key" - Licencia inválida o expirada
  • "License validation failed" - Error en la validación

2.7 Componentes adicionales

El WebComponent Jaak Stamps incluye los siguientes módulos complementarios:

Servicios de Core

  • CameraService: Manejo de múltiples cámaras, detección de dispositivos, y optimización de resolución
  • DetectionService: Carga de modelos ONNX, inferencia en tiempo real, y clasificación de documentos
  • StateManagerService: Gestión del flujo de captura multi-etapa (frente → reverso → completado)
  • EventBusService: Sistema de comunicación entre componentes con eventos tipados
  • LoggerService: Sistema de logging con diferentes niveles (debug, info, warn, error)
  • ServiceContainer: Contenedor de inyección de dependencias para gestión centralizada de servicios

Estrategias de Dispositivo

  • HighPerformanceDeviceStrategy: Estrategia optimizada para dispositivos de alto rendimiento
  • LowMemoryDeviceStrategy: Estrategia optimizada para dispositivos con recursos limitados
  • DeviceStrategyFactory: Factory para seleccionar automáticamente la estrategia apropiada

Interfaces y Contratos

  • ICameraService: Contrato para servicios de manejo de cámara
  • IDetectionService: Contrato para servicios de detección de documentos
  • IStateManager: Contrato para gestión de estado de captura
  • IEventBus: Contrato para sistema de eventos
  • ILogger: Contrato para servicios de logging

Utilidades de Interfaz

  • Selector de cámaras: Interfaz desplegable para cambio de cámara en tiempo real
  • Monitor de rendimiento: Métricas en tiempo real (FPS, memoria, tiempo de inferencia) - solo en modo debug
  • Animaciones de estado: Feedback visual para captura, volteo de documento, y éxito
  • Máscara de alineación: Guía visual para posicionamiento correcto del documento

2.8 Pruebas y validación

a) Casos de prueba

Caso de PruebaEntrada/ConfiguraciónResultado EsperadoCriterio de Éxito
Inicialización básica<jaak-stamps></jaak-stamps>Componente se inicializa con configuración por defectoEvento isReady se dispara con true
Captura frontal exitosaDocumento ID visible en cámara por >1 segundoImagen frontal capturada y transición a captura traseraAnimación "voltea tu identificación" aparece
Captura completaCaptura exitosa de frente y reversoProceso completado con ambas imágenesEvento captureCompleted con metadata correcta
Detección de pasaporteuse-document-classification="true" con pasaporteSalta captura de reverso automáticamentebackCaptureSkipped: true en metadata
Cambio de cámaraDispositivo con múltiples cámarasInterfaz de selección disponible y funcionalCambio exitoso sin interrumpir captura
Modo debugdebug="true"Métricas de rendimiento y cajas de detección visiblesMonitor de performance y overlay de detección activos
Manejo de erroresSin permisos de cámaraError manejado graciosamenteEstado de error mostrado al usuario

2.9 Solución de problemas

a) Problemas comunes

Problema:El componente no se inicializa correctamente
Descripción:El evento isReady nunca se dispara o se dispara con false
Causas posibles:1. Falta de permisos de cámara
2. Navegador no compatible
3. Conexión no HTTPS
4. Error en descarga de modelos
5. Licencia inválida o no proporcionada
Solución:1. Verificar que la página esté servida sobre HTTPS
2. Confirmar permisos de cámara en el navegador
3. Verificar conectividad a internet
4. Revisar consola del navegador para errores
5. Validar que la licencia sea correcta
Problema:La detección de documentos es muy lenta
Descripción:El procesamiento de frames toma >200ms causando lag en la interfaz
Causas posibles:1. Dispositivo con poca memoria
2. Múltiples pestañas/aplicaciones abiertas
3. Resolución de cámara muy alta
Solución:1. Cerrar aplicaciones innecesarias
2. Usar debug="true" para monitorear rendimiento
3. Considerar reducir maskSize para menos procesamiento
Problema:Las imágenes capturadas están cortadas o mal alineadas
Descripción:Las imágenes resultantes no incluyen todo el documento
Causas posibles:1. cropMargin muy pequeño
2. alignmentTolerance muy estricto
3. Movimiento durante captura
Solución:1. Incrementar cropMargin (ej: 10-20)
2. Aumentar alignmentTolerance (ej: 15-20)
3. Instruir al usuario a mantener el documento quieto

b) Códigos de error específicos

CódigoDescripciónCausaSolución
Camera permission errorPermisos de cámara denegadosUsuario rechazó permisos o política del navegadorMostrar instrucciones para habilitar permisos manualmente
Camera with ID X not foundCámara específica no encontradaDispositivo sin cámara o cámara desconectadaVerificar disponibilidad de cámara, usar cámara automática
Error al enumerar cámarasNo se pudieron listar cámarasPermisos insuficientes o navegador no compatibleVerificar permisos y soporte del navegador
Error al cargar preferenciaFallo al cargar configuración guardadaDatos corruptos en localStorageLimpiar localStorage o usar configuración por defecto
Error al guardar preferenciaFallo al guardar configuraciónProblemas de almacenamiento localVerificar capacidad de almacenamiento del navegador
License key is requiredNo se proporcionó licenciaFalta el atributo licenseAgregar licencia válida al componente
Invalid license keyLicencia inválidaLicencia incorrecta o expiradaContactar a soporte para obtener licencia válida

Contacta al equipo de soporte ([email protected]) cuando:

  • Los pasos de troubleshooting no resuelven el problema
  • Recibes errores no documentados
  • Necesitas configuraciones especiales para tu caso de uso
  • Experimentas problemas de rendimiento persistentes

Información a incluir: Logs del navegador, configuración del componente, pasos para reproducir el problema, tipo de dispositivo y navegador


2.10 Consideraciones importantes

a) Seguridad

  • Privacidad de datos: Las imágenes se procesan localmente en el navegador, no se envían automáticamente a servidores externos
  • Permisos de cámara: Siempre solicitar permisos explícitos del usuario antes de acceder a la cámara
  • HTTPS obligatorio: El componente requiere HTTPS para acceso a MediaStream API
  • Validación de entrada: Validar propiedades del componente para evitar configuraciones inseguras
  • Manejo de errores: Implementar manejo robusto de errores para evitar exposición de información sensible

b) Rendimiento

  • Optimización automática: El componente detecta automáticamente las capacidades del dispositivo y ajusta el rendimiento
  • Precarga de modelos: Usar preloadModel() para mejorar la experiencia del usuario
  • Gestión de memoria: Los modelos se descargan una sola vez y se reutilizan
  • Frame skipping: Implementado automáticamente para mantener fluidez en dispositivos lentos

c) Compatibilidad

  • Navegadores soportados: Chrome 67+, Firefox 63+, Safari 12+, Edge 79+
  • Dispositivos móviles: Optimizado para iOS Safari y Android Chrome
  • Resolución adaptativa: Se ajusta automáticamente a diferentes tamaños de pantalla
  • WebComponents nativos: No requiere polyfills en navegadores modernos

d) Flujo de Trabajo

Flujo Estándar (useDocumentClassification = false)

  1. Inicialización: El componente se prepara para la detección
  2. Iniciar captura: El modelo de detección se carga automáticamente
  3. Captura frontal: El usuario posiciona el documento y se captura automáticamente
  4. Solicitud de reverso: Siempre solicita voltear el documento para captura del reverso
  5. Botón omitir reverso: Disponible para omitir manualmente la captura del reverso
  6. Completado: Emite evento captureCompleted con las imágenes correspondientes

Flujo de Clasificación Inteligente (useDocumentClassification = true)

  1. Inicialización: El componente se prepara para la detección
  2. Iniciar captura: Los modelos de detección y clasificación se cargan automáticamente
  3. Captura frontal: El usuario posiciona el documento y se captura automáticamente
  4. Clasificación automática: El sistema determina si el documento requiere captura del reverso
  5. Flujo adaptativo:
    • Si es pasaporte: El proceso se completa automáticamente (sin reverso)
    • Si es otro documento: Solicita voltear el documento para captura del reverso
  6. Completado: Emite evento captureCompleted con las imágenes correspondientes

Flujo de Precarga (Recomendado)

  1. Precarga de modelo: Llamar a preloadModel() para cargar modelos en memoria
  2. Inicio optimizado: Al iniciar captura, utiliza modelos ya cargados
  3. Captura frontal: Detección y clasificación más rápida con modelos precargados
  4. Flujo inteligente:
    • Documento sin reverso: Proceso completado inmediatamente
    • Documento con reverso: Solicita captura del reverso
  5. Completado: Emite evento captureCompleted con todas las imágenes

3. Anexos

Anexo A. Glosario de términos

TérminoDefinición
ONNXOpen Neural Network Exchange - Formato estándar para modelos de machine learning
WebComponentEstándar web que permite crear elementos HTML personalizados reutilizables
MediaStream APIAPI del navegador para acceso a cámaras y micrófonos
Shadow DOMTecnología que permite encapsulación de estilos y comportamiento en WebComponents
InferenceProceso de ejecutar un modelo de IA para obtener predicciones
Detection BoxRectángulo que define la ubicación de un documento detectado en la imagen
Alignment ToleranceTolerancia en píxeles para considerar un documento como correctamente alineado
Crop MarginMargen adicional alrededor del documento detectado al recortar la imagen
OpenTelemetryFramework de observabilidad para instrumentación, generación y exportación de datos de telemetría
OTLPOpenTelemetry Protocol - Protocolo estándar para transmisión de datos de telemetría
Trace ContextEstándar W3C para propagación de contexto de trazas distribuidas