@jaak.ai/stamps 2.0.0-dev.22 → 2.0.0-dev.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const JaakStamps = class {
12
12
  debug = false;
13
13
  alignmentTolerance = 10; // Tolerancia en píxeles para considerar un lado alineado (escalado para 320x320)
14
14
  maskSize = 90; // Porcentaje del video que ocupará la máscara (default 90%)
15
+ cropMargin = 0; // Margen en píxeles para el recorte del documento (default 0)
15
16
  captureCompleted;
16
17
  isReady;
17
18
  isVideoActive = false;
@@ -45,6 +46,8 @@ const JaakStamps = class {
45
46
  animationId;
46
47
  hasScreenshotTaken = false;
47
48
  lastDetectedBox;
49
+ mobileNetSession;
50
+ mobileNetClassMap;
48
51
  // Performance optimization properties
49
52
  frameSkipCounter = 0;
50
53
  FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
@@ -57,7 +60,9 @@ const JaakStamps = class {
57
60
  preprocessCtx;
58
61
  captureCanvas;
59
62
  captureCtx;
60
- MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/detector-nano.onnx";
63
+ MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v1.onnx";
64
+ MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
65
+ MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
61
66
  INPUT_SIZE = 320;
62
67
  CONFIDENCE_THRESHOLD = 0.6;
63
68
  // ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
@@ -73,6 +78,12 @@ const JaakStamps = class {
73
78
  this.maskSize = 90;
74
79
  }
75
80
  }
81
+ validateCropMargin() {
82
+ if (this.cropMargin < 0 || this.cropMargin > 100) {
83
+ console.warn(`cropMargin debe estar entre 0 y 100. Valor actual: ${this.cropMargin}. Usando valor por defecto: 0`);
84
+ this.cropMargin = 0;
85
+ }
86
+ }
76
87
  emitReadyEvent() {
77
88
  const isDocumentReady = !!window.ort && this.isModelPreloaded;
78
89
  this.isReady.emit(isDocumentReady);
@@ -87,6 +98,7 @@ const JaakStamps = class {
87
98
  }
88
99
  async componentDidLoad() {
89
100
  this.validateMaskSize();
101
+ this.validateCropMargin();
90
102
  if (!window.ort) {
91
103
  const script = document.createElement('script');
92
104
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
@@ -256,25 +268,27 @@ const JaakStamps = class {
256
268
  }
257
269
  try {
258
270
  this.isLoading = true;
259
- this.statusMessage = "Precargando modelo de detección...";
271
+ this.statusMessage = "Precargando modelos...";
260
272
  this.statusColor = "#007bff";
261
273
  const modelPath = this.MODEL_PATH;
262
- this.debugLog('🤖 Preloading model:', modelPath);
274
+ this.debugLog('🤖 Preloading detection model:', modelPath);
263
275
  // Configure ONNX Runtime with device-specific optimizations
264
276
  const sessionOptions = this.getSessionOptions();
265
277
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
278
+ // Preload MobileNet model and classes
279
+ await this.loadMobileNetModel();
266
280
  this.isModelPreloaded = true;
267
281
  this.isLoading = false;
268
- this.statusMessage = "Modelo precargado. Listo para comenzar detección";
282
+ this.statusMessage = "Modelos precargados. Listo para comenzar detección";
269
283
  this.statusColor = "#28a745";
270
284
  this.emitReadyEvent();
271
- this.debugLog('✅ Model preloaded successfully');
272
- return { success: true, message: 'Model preloaded successfully' };
285
+ this.debugLog('✅ Models preloaded successfully');
286
+ return { success: true, message: 'Models preloaded successfully' };
273
287
  }
274
288
  catch (error) {
275
- this.debugLog('❌ Error preloading model:', error);
289
+ this.debugLog('❌ Error preloading models:', error);
276
290
  this.isLoading = false;
277
- this.statusMessage = "Error al precargar el modelo";
291
+ this.statusMessage = "Error al precargar los modelos";
278
292
  this.statusColor = "#ff6b6b";
279
293
  return { success: false, error: error.message };
280
294
  }
@@ -284,6 +298,80 @@ const JaakStamps = class {
284
298
  this.completeProcess(true);
285
299
  }
286
300
  }
301
+ async loadMobileNetModel() {
302
+ try {
303
+ this.debugLog('🤖 Loading MobileNet model...');
304
+ // Load class map
305
+ const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
306
+ if (!classResponse.ok) {
307
+ throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
308
+ }
309
+ this.mobileNetClassMap = await classResponse.json();
310
+ this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
311
+ // Load model
312
+ const sessionOptions = this.getSessionOptions();
313
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
314
+ this.debugLog('✅ MobileNet model loaded successfully');
315
+ }
316
+ catch (error) {
317
+ this.debugLog('❌ Error loading MobileNet model:', error);
318
+ throw error;
319
+ }
320
+ }
321
+ preprocessMobileNet(canvas) {
322
+ // Create a temporary canvas for MobileNet preprocessing (224x224)
323
+ const tempCanvas = document.createElement('canvas');
324
+ tempCanvas.width = 224;
325
+ tempCanvas.height = 224;
326
+ const tempCtx = tempCanvas.getContext('2d');
327
+ // Draw the cropped image onto the 224x224 canvas
328
+ tempCtx.drawImage(canvas, 0, 0, 224, 224);
329
+ // Get image data and preprocess for MobileNet
330
+ const imageData = tempCtx.getImageData(0, 0, 224, 224);
331
+ const data = imageData.data;
332
+ const hw = 224 * 224;
333
+ const arr = new Float32Array(3 * hw);
334
+ // Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
335
+ for (let i = 0; i < hw; i++) {
336
+ arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
337
+ arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
338
+ arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
339
+ }
340
+ return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
341
+ }
342
+ async classifyDocument(canvas) {
343
+ if (!this.mobileNetSession || !this.mobileNetClassMap) {
344
+ this.debugLog('⚠️ MobileNet model not loaded');
345
+ return null;
346
+ }
347
+ try {
348
+ this.debugLog('🔍 Classifying document...');
349
+ // Preprocess image for MobileNet
350
+ const inputTensor = this.preprocessMobileNet(canvas);
351
+ // Run inference
352
+ const feeds = { input: inputTensor };
353
+ const results = await this.mobileNetSession.run(feeds);
354
+ const output = results[Object.keys(results)[0]].data;
355
+ // Find the class with highest confidence
356
+ const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
357
+ const confidence = output[maxIdx];
358
+ const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
359
+ this.debugLog('📄 Document classification result:', {
360
+ class: className,
361
+ confidence: confidence.toFixed(3),
362
+ classIndex: maxIdx
363
+ });
364
+ return {
365
+ class: className,
366
+ confidence: confidence,
367
+ classIndex: maxIdx
368
+ };
369
+ }
370
+ catch (error) {
371
+ this.debugLog('❌ Error classifying document:', error);
372
+ return null;
373
+ }
374
+ }
287
375
  cleanup() {
288
376
  if (this.animationId) {
289
377
  cancelAnimationFrame(this.animationId);
@@ -311,11 +399,16 @@ const JaakStamps = class {
311
399
  this.captureCtx = undefined;
312
400
  this.captureCanvas = undefined;
313
401
  }
314
- // Disposed ONNX session
402
+ // Disposed ONNX sessions
315
403
  if (this.session) {
316
404
  this.session.release?.();
317
405
  this.session = undefined;
318
406
  }
407
+ if (this.mobileNetSession) {
408
+ this.mobileNetSession.release?.();
409
+ this.mobileNetSession = undefined;
410
+ }
411
+ this.mobileNetClassMap = undefined;
319
412
  this.isModelPreloaded = false;
320
413
  this.debugLog('🧹 Canvas pool cleaned up');
321
414
  }
@@ -446,18 +539,22 @@ const JaakStamps = class {
446
539
  // Check if model is already preloaded
447
540
  if (!this.session) {
448
541
  this.isLoading = true;
449
- this.statusMessage = "Cargando modelo de detección...";
542
+ this.statusMessage = "Cargando modelos...";
450
543
  this.statusColor = "#007bff";
451
544
  const modelPath = this.MODEL_PATH;
452
- this.debugLog('🤖 Loading model:', modelPath);
545
+ this.debugLog('🤖 Loading detection model:', modelPath);
453
546
  const sessionOptions = this.getSessionOptions();
454
547
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
548
+ // Load MobileNet model if not already loaded
549
+ if (!this.mobileNetSession) {
550
+ await this.loadMobileNetModel();
551
+ }
455
552
  this.isModelPreloaded = true;
456
553
  this.emitReadyEvent();
457
554
  }
458
555
  else {
459
- this.debugLog('🚀 Using preloaded model');
460
- this.statusMessage = "Usando modelo precargado...";
556
+ this.debugLog('🚀 Using preloaded models');
557
+ this.statusMessage = "Usando modelos precargados...";
461
558
  this.statusColor = "#007bff";
462
559
  }
463
560
  this.statusMessage = "Configurando cámara...";
@@ -676,7 +773,9 @@ const JaakStamps = class {
676
773
  corners?.forEach(corner => corner.classList.add('perfect-match'));
677
774
  if (!this.hasScreenshotTaken) {
678
775
  this.lastDetectedBox = bestBox;
679
- this.takeScreenshot();
776
+ this.takeScreenshot().catch(error => {
777
+ this.debugLog('❌ Error taking screenshot:', error);
778
+ });
680
779
  this.hasScreenshotTaken = true;
681
780
  // Reset para permitir segunda captura
682
781
  setTimeout(() => {
@@ -759,7 +858,7 @@ const JaakStamps = class {
759
858
  ctx.strokeRect(x, y, w, h);
760
859
  });
761
860
  }
762
- takeScreenshot() {
861
+ async takeScreenshot() {
763
862
  if (!this.videoRef || !this.lastDetectedBox)
764
863
  return;
765
864
  // Activar animación
@@ -775,10 +874,10 @@ const JaakStamps = class {
775
874
  // Calcular las coordenadas de recorte basadas en la detección
776
875
  const scaleX = this.videoRef.videoWidth / this.INPUT_SIZE;
777
876
  const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
778
- const cropX = Math.max(0, this.lastDetectedBox.x * scaleX);
779
- const cropY = Math.max(0, this.lastDetectedBox.y * scaleY);
780
- const cropWidth = Math.min(this.lastDetectedBox.w * scaleX, this.videoRef.videoWidth - cropX);
781
- const cropHeight = Math.min(this.lastDetectedBox.h * scaleY, this.videoRef.videoHeight - cropY);
877
+ const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
878
+ const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
879
+ const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
880
+ const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
782
881
  // OPTIMIZATION: Create temporary canvas only for cropped version
783
882
  // (We reuse main capture canvas for full frame)
784
883
  const croppedCanvas = document.createElement('canvas');
@@ -793,6 +892,15 @@ const JaakStamps = class {
793
892
  // Captura del frente usando canvas reutilizado
794
893
  this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
795
894
  this.capturedCroppedId = croppedCanvas.toDataURL('image/png');
895
+ // Classify the cropped document
896
+ const classification = await this.classifyDocument(croppedCanvas);
897
+ if (classification && classification.class === 'passport') {
898
+ // If it's a passport, skip back capture since passports don't have a back side
899
+ this.debugLog('📄 Passport detected - skipping back capture');
900
+ this.completeProcess(true);
901
+ return;
902
+ }
903
+ // For other IDs, continue with normal flow (back capture)
796
904
  this.captureStep = 'back';
797
905
  this.statusMessage = "Voltee la identificación y muestre la parte trasera";
798
906
  this.statusColor = "#007bff";
@@ -922,7 +1030,7 @@ const JaakStamps = class {
922
1030
  this.cleanup();
923
1031
  }
924
1032
  render() {
925
- return (h("div", { key: 'd812c8d6a607b4f2889be8f84384156f67ca51e3', class: "detector-container" }, h("div", { key: '215b581f8ebafa9295133fe397f4dcaea952e2bc', class: "video-container" }, h("video", { key: '2ac492b3c535857d8ee681dc0bfb91e6de25a4d0', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: this.isVideoActive ? 'block' : 'none' } }), h("canvas", { key: 'e51c2e4a4751d89cc662a26eff3d8360fa199f4b', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (h("div", { key: '2e0bb7df390e6286a758098abf929ce23e0820f2', class: "overlay-mask" }, h("div", { key: '3a8c73cef589adcf98991719e0adc0791cf51e13', class: "card-outline" }, h("div", { key: '908bdd704ff89f8ba9f2f30da068d2d2074f43ed', class: "side side-top" }), h("div", { key: 'd27227f2e03905d288cb5509cb4f8b98a5ade59e', class: "side side-right" }), h("div", { key: '7bdce198e7ff2dc459768b001edd3c52b0cc28a2', class: "side side-bottom" }), h("div", { key: '9f3c97b8d154a7e48b36accac1eb7e33ee2b3184', class: "side side-left" }), h("div", { key: '2ec0bfd6308505d8780453a22aeb9b25108b8a14', class: "corner corner-tl" }), h("div", { key: 'd6d1589901a1a2cba38451b31126524ccc009a9b', class: "corner corner-tr" }), h("div", { key: 'db4ddc071c5b29d2d5912f7a880cdf8654c0db70', class: "corner corner-bl" }), h("div", { key: 'cf9cf5d60189e512a5f4bfc08e24d2bf9ed3a499', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (h("div", { key: '0412e6f6ecca1af59710256a27d9ceb7b11c5998', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (h("button", { key: '26bc516d80fda38ea94926e574812362cd77a816', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (h("div", { key: '49c64155438f74dc3e0c5e981dac62844315180d', class: "capture-animation" })), this.showFlipAnimation && (h("div", { key: '738adb391c667de96a0c30750e75de43803ae613', class: "flip-animation" }, h("div", { key: '9df006a94a68b9d6e4dd351fb0b88cf78e62b2ec', class: "id-card-icon" }), h("div", { key: '3e1736f4aeb5b14a3edc4f52991fc34ff85169ad', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (h("div", { key: 'dc46b715ef3e4fb98e5ee3f8449520db6bb68e53', class: "success-animation" }, h("div", { key: '641dda07b6545e9ca391e8c6149c7e29251d68ae', class: "check-icon" }), h("div", { key: 'ea53a4d547be89cc0e79d5b616a289405fea1e75', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (h("div", { key: 'bf30969faeb9b6bee4099d2462b6abeee7eca1ff', class: "loading-overlay" }, h("div", { key: 'c32d185c1376975ac20640c532b0f4e593caf0c0', class: "loading-spinner" }), h("div", { key: '2c5a64af6abd379f247640498dafa01efe661c5c', class: "loading-text" }, this.statusMessage))), h("div", { key: 'd16122cf20d25e51dc7a3c91753f541f41c2747e', class: "watermark" }, h("img", { key: 'ce18db8a5997dfa6e2ebae47b9a61b665f8106bd', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1033
+ return (h("div", { key: 'd6cd4225500c44e5c7fb4f2b3dc453a055467066', class: "detector-container" }, h("div", { key: '6bd49b0bcd686648aeffb8fb95203dc0779f1d8a', class: "video-container" }, h("video", { key: 'd2666ce9d1f5b84703ac86fd4eb901f4298ed7d5', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: this.isVideoActive ? 'block' : 'none' } }), h("canvas", { key: 'e997f063450d81d2a15398f0761c9d06c28a85c9', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (h("div", { key: '4ccb8bf05091a7173255181eae1a8699fe512b56', class: "overlay-mask" }, h("div", { key: 'b35defa8c207b05e41d64c8e1eefc905d31fad55', class: "card-outline" }, h("div", { key: '6ed8dad63d6d79c1bb4a64f0b28467f5b29d778a', class: "side side-top" }), h("div", { key: '403d4a5ba681a58ad1870a4339e19b4f037a8795', class: "side side-right" }), h("div", { key: '3cb3276bb184a4b76419b4cb0b4fcb151a4df631', class: "side side-bottom" }), h("div", { key: 'ed7c9bec0e708ab282ba8b24a08d21b8126af27e', class: "side side-left" }), h("div", { key: '157be211355bf9260dc7a73d03831dbb1a2945e3', class: "corner corner-tl" }), h("div", { key: 'b19af5f319333ff05f981063af939d2c4cfc9b5f', class: "corner corner-tr" }), h("div", { key: '6fcd3e2c8842261dcd32c36e6df5b2f3dd0c8fc8', class: "corner corner-bl" }), h("div", { key: '7caab5eaee62f3f9039075b3748083c89c8127e0', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (h("div", { key: 'e3dd42ddb75b22aeef438d8651955f3dc039f00c', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (h("button", { key: '54b8651817cd52bad2f34ac1413a4b1e21166cee', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (h("div", { key: '853707a4fa71eeacbee7985e619fca80d6522cbd', class: "capture-animation" })), this.showFlipAnimation && (h("div", { key: '246ed3f5dc57ee57345105cdebe5f950d5ff4c90', class: "flip-animation" }, h("div", { key: 'f03c6c87c8f5e98a32176cca061d369a521da2e8', class: "id-card-icon" }), h("div", { key: 'e357785b076a9e41ea4f9b3d9c14e847bacc9604', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (h("div", { key: 'df3adaaecd3d547938638c58ccc0e89f8325fc23', class: "success-animation" }, h("div", { key: 'a023985b8afd6ed72f9e1c2ee63e2c7ba9fb960b', class: "check-icon" }), h("div", { key: '739ff7f329f2131061a6860529c07c8819800aab', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (h("div", { key: '9b02df640528a19cb15e5dbabf9ecb43afbceffa', class: "loading-overlay" }, h("div", { key: '3652c8e355ea7fe623c478229bf548bdf30e399a', class: "loading-spinner" }), h("div", { key: '334f4469b5672f6af78b8679c15ed8c59ecefa85', class: "loading-text" }, this.statusMessage))), h("div", { key: 'a0b7c9b92771cff96e97bd67da2fd3090b080f8a', class: "watermark" }, h("img", { key: 'b5c8f0de1d8138d4a0c80526fd33b2daefa86b16', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
926
1034
  }
927
1035
  };
928
1036
  JaakStamps.style = myComponentCss;