@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.
@@ -14,6 +14,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
14
14
  debug = false;
15
15
  alignmentTolerance = 10; // Tolerancia en píxeles para considerar un lado alineado (escalado para 320x320)
16
16
  maskSize = 90; // Porcentaje del video que ocupará la máscara (default 90%)
17
+ cropMargin = 0; // Margen en píxeles para el recorte del documento (default 0)
17
18
  captureCompleted;
18
19
  isReady;
19
20
  isVideoActive = false;
@@ -47,6 +48,8 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
47
48
  animationId;
48
49
  hasScreenshotTaken = false;
49
50
  lastDetectedBox;
51
+ mobileNetSession;
52
+ mobileNetClassMap;
50
53
  // Performance optimization properties
51
54
  frameSkipCounter = 0;
52
55
  FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
@@ -59,7 +62,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
59
62
  preprocessCtx;
60
63
  captureCanvas;
61
64
  captureCtx;
62
- MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/detector-nano.onnx";
65
+ MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v1.onnx";
66
+ MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
67
+ MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
63
68
  INPUT_SIZE = 320;
64
69
  CONFIDENCE_THRESHOLD = 0.6;
65
70
  // ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
@@ -75,6 +80,12 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
75
80
  this.maskSize = 90;
76
81
  }
77
82
  }
83
+ validateCropMargin() {
84
+ if (this.cropMargin < 0 || this.cropMargin > 100) {
85
+ console.warn(`cropMargin debe estar entre 0 y 100. Valor actual: ${this.cropMargin}. Usando valor por defecto: 0`);
86
+ this.cropMargin = 0;
87
+ }
88
+ }
78
89
  emitReadyEvent() {
79
90
  const isDocumentReady = !!window.ort && this.isModelPreloaded;
80
91
  this.isReady.emit(isDocumentReady);
@@ -89,6 +100,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
89
100
  }
90
101
  async componentDidLoad() {
91
102
  this.validateMaskSize();
103
+ this.validateCropMargin();
92
104
  if (!window.ort) {
93
105
  const script = document.createElement('script');
94
106
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
@@ -258,25 +270,27 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
258
270
  }
259
271
  try {
260
272
  this.isLoading = true;
261
- this.statusMessage = "Precargando modelo de detección...";
273
+ this.statusMessage = "Precargando modelos...";
262
274
  this.statusColor = "#007bff";
263
275
  const modelPath = this.MODEL_PATH;
264
- this.debugLog('🤖 Preloading model:', modelPath);
276
+ this.debugLog('🤖 Preloading detection model:', modelPath);
265
277
  // Configure ONNX Runtime with device-specific optimizations
266
278
  const sessionOptions = this.getSessionOptions();
267
279
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
280
+ // Preload MobileNet model and classes
281
+ await this.loadMobileNetModel();
268
282
  this.isModelPreloaded = true;
269
283
  this.isLoading = false;
270
- this.statusMessage = "Modelo precargado. Listo para comenzar detección";
284
+ this.statusMessage = "Modelos precargados. Listo para comenzar detección";
271
285
  this.statusColor = "#28a745";
272
286
  this.emitReadyEvent();
273
- this.debugLog('✅ Model preloaded successfully');
274
- return { success: true, message: 'Model preloaded successfully' };
287
+ this.debugLog('✅ Models preloaded successfully');
288
+ return { success: true, message: 'Models preloaded successfully' };
275
289
  }
276
290
  catch (error) {
277
- this.debugLog('❌ Error preloading model:', error);
291
+ this.debugLog('❌ Error preloading models:', error);
278
292
  this.isLoading = false;
279
- this.statusMessage = "Error al precargar el modelo";
293
+ this.statusMessage = "Error al precargar los modelos";
280
294
  this.statusColor = "#ff6b6b";
281
295
  return { success: false, error: error.message };
282
296
  }
@@ -286,6 +300,80 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
286
300
  this.completeProcess(true);
287
301
  }
288
302
  }
303
+ async loadMobileNetModel() {
304
+ try {
305
+ this.debugLog('🤖 Loading MobileNet model...');
306
+ // Load class map
307
+ const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
308
+ if (!classResponse.ok) {
309
+ throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
310
+ }
311
+ this.mobileNetClassMap = await classResponse.json();
312
+ this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
313
+ // Load model
314
+ const sessionOptions = this.getSessionOptions();
315
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
316
+ this.debugLog('✅ MobileNet model loaded successfully');
317
+ }
318
+ catch (error) {
319
+ this.debugLog('❌ Error loading MobileNet model:', error);
320
+ throw error;
321
+ }
322
+ }
323
+ preprocessMobileNet(canvas) {
324
+ // Create a temporary canvas for MobileNet preprocessing (224x224)
325
+ const tempCanvas = document.createElement('canvas');
326
+ tempCanvas.width = 224;
327
+ tempCanvas.height = 224;
328
+ const tempCtx = tempCanvas.getContext('2d');
329
+ // Draw the cropped image onto the 224x224 canvas
330
+ tempCtx.drawImage(canvas, 0, 0, 224, 224);
331
+ // Get image data and preprocess for MobileNet
332
+ const imageData = tempCtx.getImageData(0, 0, 224, 224);
333
+ const data = imageData.data;
334
+ const hw = 224 * 224;
335
+ const arr = new Float32Array(3 * hw);
336
+ // Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
337
+ for (let i = 0; i < hw; i++) {
338
+ arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
339
+ arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
340
+ arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
341
+ }
342
+ return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
343
+ }
344
+ async classifyDocument(canvas) {
345
+ if (!this.mobileNetSession || !this.mobileNetClassMap) {
346
+ this.debugLog('⚠️ MobileNet model not loaded');
347
+ return null;
348
+ }
349
+ try {
350
+ this.debugLog('🔍 Classifying document...');
351
+ // Preprocess image for MobileNet
352
+ const inputTensor = this.preprocessMobileNet(canvas);
353
+ // Run inference
354
+ const feeds = { input: inputTensor };
355
+ const results = await this.mobileNetSession.run(feeds);
356
+ const output = results[Object.keys(results)[0]].data;
357
+ // Find the class with highest confidence
358
+ const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
359
+ const confidence = output[maxIdx];
360
+ const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
361
+ this.debugLog('📄 Document classification result:', {
362
+ class: className,
363
+ confidence: confidence.toFixed(3),
364
+ classIndex: maxIdx
365
+ });
366
+ return {
367
+ class: className,
368
+ confidence: confidence,
369
+ classIndex: maxIdx
370
+ };
371
+ }
372
+ catch (error) {
373
+ this.debugLog('❌ Error classifying document:', error);
374
+ return null;
375
+ }
376
+ }
289
377
  cleanup() {
290
378
  if (this.animationId) {
291
379
  cancelAnimationFrame(this.animationId);
@@ -313,11 +401,16 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
313
401
  this.captureCtx = undefined;
314
402
  this.captureCanvas = undefined;
315
403
  }
316
- // Disposed ONNX session
404
+ // Disposed ONNX sessions
317
405
  if (this.session) {
318
406
  this.session.release?.();
319
407
  this.session = undefined;
320
408
  }
409
+ if (this.mobileNetSession) {
410
+ this.mobileNetSession.release?.();
411
+ this.mobileNetSession = undefined;
412
+ }
413
+ this.mobileNetClassMap = undefined;
321
414
  this.isModelPreloaded = false;
322
415
  this.debugLog('🧹 Canvas pool cleaned up');
323
416
  }
@@ -448,18 +541,22 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
448
541
  // Check if model is already preloaded
449
542
  if (!this.session) {
450
543
  this.isLoading = true;
451
- this.statusMessage = "Cargando modelo de detección...";
544
+ this.statusMessage = "Cargando modelos...";
452
545
  this.statusColor = "#007bff";
453
546
  const modelPath = this.MODEL_PATH;
454
- this.debugLog('🤖 Loading model:', modelPath);
547
+ this.debugLog('🤖 Loading detection model:', modelPath);
455
548
  const sessionOptions = this.getSessionOptions();
456
549
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
550
+ // Load MobileNet model if not already loaded
551
+ if (!this.mobileNetSession) {
552
+ await this.loadMobileNetModel();
553
+ }
457
554
  this.isModelPreloaded = true;
458
555
  this.emitReadyEvent();
459
556
  }
460
557
  else {
461
- this.debugLog('🚀 Using preloaded model');
462
- this.statusMessage = "Usando modelo precargado...";
558
+ this.debugLog('🚀 Using preloaded models');
559
+ this.statusMessage = "Usando modelos precargados...";
463
560
  this.statusColor = "#007bff";
464
561
  }
465
562
  this.statusMessage = "Configurando cámara...";
@@ -678,7 +775,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
678
775
  corners?.forEach(corner => corner.classList.add('perfect-match'));
679
776
  if (!this.hasScreenshotTaken) {
680
777
  this.lastDetectedBox = bestBox;
681
- this.takeScreenshot();
778
+ this.takeScreenshot().catch(error => {
779
+ this.debugLog('❌ Error taking screenshot:', error);
780
+ });
682
781
  this.hasScreenshotTaken = true;
683
782
  // Reset para permitir segunda captura
684
783
  setTimeout(() => {
@@ -761,7 +860,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
761
860
  ctx.strokeRect(x, y, w, h);
762
861
  });
763
862
  }
764
- takeScreenshot() {
863
+ async takeScreenshot() {
765
864
  if (!this.videoRef || !this.lastDetectedBox)
766
865
  return;
767
866
  // Activar animación
@@ -777,10 +876,10 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
777
876
  // Calcular las coordenadas de recorte basadas en la detección
778
877
  const scaleX = this.videoRef.videoWidth / this.INPUT_SIZE;
779
878
  const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
780
- const cropX = Math.max(0, this.lastDetectedBox.x * scaleX);
781
- const cropY = Math.max(0, this.lastDetectedBox.y * scaleY);
782
- const cropWidth = Math.min(this.lastDetectedBox.w * scaleX, this.videoRef.videoWidth - cropX);
783
- const cropHeight = Math.min(this.lastDetectedBox.h * scaleY, this.videoRef.videoHeight - cropY);
879
+ const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
880
+ const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
881
+ const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
882
+ const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
784
883
  // OPTIMIZATION: Create temporary canvas only for cropped version
785
884
  // (We reuse main capture canvas for full frame)
786
885
  const croppedCanvas = document.createElement('canvas');
@@ -795,6 +894,15 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
795
894
  // Captura del frente usando canvas reutilizado
796
895
  this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
797
896
  this.capturedCroppedId = croppedCanvas.toDataURL('image/png');
897
+ // Classify the cropped document
898
+ const classification = await this.classifyDocument(croppedCanvas);
899
+ if (classification && classification.class === 'passport') {
900
+ // If it's a passport, skip back capture since passports don't have a back side
901
+ this.debugLog('📄 Passport detected - skipping back capture');
902
+ this.completeProcess(true);
903
+ return;
904
+ }
905
+ // For other IDs, continue with normal flow (back capture)
798
906
  this.captureStep = 'back';
799
907
  this.statusMessage = "Voltee la identificación y muestre la parte trasera";
800
908
  this.statusColor = "#007bff";
@@ -924,13 +1032,14 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
924
1032
  this.cleanup();
925
1033
  }
926
1034
  render() {
927
- 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" })))));
1035
+ 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" })))));
928
1036
  }
929
1037
  static get style() { return myComponentCss; }
930
1038
  }, [1, "jaak-stamps", {
931
1039
  "debug": [4],
932
1040
  "alignmentTolerance": [2, "alignment-tolerance"],
933
1041
  "maskSize": [2, "mask-size"],
1042
+ "cropMargin": [2, "crop-margin"],
934
1043
  "isVideoActive": [32],
935
1044
  "statusMessage": [32],
936
1045
  "statusColor": [32],