@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.
@@ -4,6 +4,7 @@ export class JaakStamps {
4
4
  debug = false;
5
5
  alignmentTolerance = 10; // Tolerancia en píxeles para considerar un lado alineado (escalado para 320x320)
6
6
  maskSize = 90; // Porcentaje del video que ocupará la máscara (default 90%)
7
+ cropMargin = 0; // Margen en píxeles para el recorte del documento (default 0)
7
8
  captureCompleted;
8
9
  isReady;
9
10
  isVideoActive = false;
@@ -37,6 +38,8 @@ export class JaakStamps {
37
38
  animationId;
38
39
  hasScreenshotTaken = false;
39
40
  lastDetectedBox;
41
+ mobileNetSession;
42
+ mobileNetClassMap;
40
43
  // Performance optimization properties
41
44
  frameSkipCounter = 0;
42
45
  FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
@@ -49,7 +52,9 @@ export class JaakStamps {
49
52
  preprocessCtx;
50
53
  captureCanvas;
51
54
  captureCtx;
52
- MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/detector-nano.onnx";
55
+ MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v1.onnx";
56
+ MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
57
+ MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
53
58
  INPUT_SIZE = 320;
54
59
  CONFIDENCE_THRESHOLD = 0.6;
55
60
  // ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
@@ -65,6 +70,12 @@ export class JaakStamps {
65
70
  this.maskSize = 90;
66
71
  }
67
72
  }
73
+ validateCropMargin() {
74
+ if (this.cropMargin < 0 || this.cropMargin > 100) {
75
+ console.warn(`cropMargin debe estar entre 0 y 100. Valor actual: ${this.cropMargin}. Usando valor por defecto: 0`);
76
+ this.cropMargin = 0;
77
+ }
78
+ }
68
79
  emitReadyEvent() {
69
80
  const isDocumentReady = !!window.ort && this.isModelPreloaded;
70
81
  this.isReady.emit(isDocumentReady);
@@ -79,6 +90,7 @@ export class JaakStamps {
79
90
  }
80
91
  async componentDidLoad() {
81
92
  this.validateMaskSize();
93
+ this.validateCropMargin();
82
94
  if (!window.ort) {
83
95
  const script = document.createElement('script');
84
96
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
@@ -248,25 +260,27 @@ export class JaakStamps {
248
260
  }
249
261
  try {
250
262
  this.isLoading = true;
251
- this.statusMessage = "Precargando modelo de detección...";
263
+ this.statusMessage = "Precargando modelos...";
252
264
  this.statusColor = "#007bff";
253
265
  const modelPath = this.MODEL_PATH;
254
- this.debugLog('🤖 Preloading model:', modelPath);
266
+ this.debugLog('🤖 Preloading detection model:', modelPath);
255
267
  // Configure ONNX Runtime with device-specific optimizations
256
268
  const sessionOptions = this.getSessionOptions();
257
269
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
270
+ // Preload MobileNet model and classes
271
+ await this.loadMobileNetModel();
258
272
  this.isModelPreloaded = true;
259
273
  this.isLoading = false;
260
- this.statusMessage = "Modelo precargado. Listo para comenzar detección";
274
+ this.statusMessage = "Modelos precargados. Listo para comenzar detección";
261
275
  this.statusColor = "#28a745";
262
276
  this.emitReadyEvent();
263
- this.debugLog('✅ Model preloaded successfully');
264
- return { success: true, message: 'Model preloaded successfully' };
277
+ this.debugLog('✅ Models preloaded successfully');
278
+ return { success: true, message: 'Models preloaded successfully' };
265
279
  }
266
280
  catch (error) {
267
- this.debugLog('❌ Error preloading model:', error);
281
+ this.debugLog('❌ Error preloading models:', error);
268
282
  this.isLoading = false;
269
- this.statusMessage = "Error al precargar el modelo";
283
+ this.statusMessage = "Error al precargar los modelos";
270
284
  this.statusColor = "#ff6b6b";
271
285
  return { success: false, error: error.message };
272
286
  }
@@ -276,6 +290,80 @@ export class JaakStamps {
276
290
  this.completeProcess(true);
277
291
  }
278
292
  }
293
+ async loadMobileNetModel() {
294
+ try {
295
+ this.debugLog('🤖 Loading MobileNet model...');
296
+ // Load class map
297
+ const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
298
+ if (!classResponse.ok) {
299
+ throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
300
+ }
301
+ this.mobileNetClassMap = await classResponse.json();
302
+ this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
303
+ // Load model
304
+ const sessionOptions = this.getSessionOptions();
305
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
306
+ this.debugLog('✅ MobileNet model loaded successfully');
307
+ }
308
+ catch (error) {
309
+ this.debugLog('❌ Error loading MobileNet model:', error);
310
+ throw error;
311
+ }
312
+ }
313
+ preprocessMobileNet(canvas) {
314
+ // Create a temporary canvas for MobileNet preprocessing (224x224)
315
+ const tempCanvas = document.createElement('canvas');
316
+ tempCanvas.width = 224;
317
+ tempCanvas.height = 224;
318
+ const tempCtx = tempCanvas.getContext('2d');
319
+ // Draw the cropped image onto the 224x224 canvas
320
+ tempCtx.drawImage(canvas, 0, 0, 224, 224);
321
+ // Get image data and preprocess for MobileNet
322
+ const imageData = tempCtx.getImageData(0, 0, 224, 224);
323
+ const data = imageData.data;
324
+ const hw = 224 * 224;
325
+ const arr = new Float32Array(3 * hw);
326
+ // Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
327
+ for (let i = 0; i < hw; i++) {
328
+ arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
329
+ arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
330
+ arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
331
+ }
332
+ return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
333
+ }
334
+ async classifyDocument(canvas) {
335
+ if (!this.mobileNetSession || !this.mobileNetClassMap) {
336
+ this.debugLog('⚠️ MobileNet model not loaded');
337
+ return null;
338
+ }
339
+ try {
340
+ this.debugLog('🔍 Classifying document...');
341
+ // Preprocess image for MobileNet
342
+ const inputTensor = this.preprocessMobileNet(canvas);
343
+ // Run inference
344
+ const feeds = { input: inputTensor };
345
+ const results = await this.mobileNetSession.run(feeds);
346
+ const output = results[Object.keys(results)[0]].data;
347
+ // Find the class with highest confidence
348
+ const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
349
+ const confidence = output[maxIdx];
350
+ const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
351
+ this.debugLog('📄 Document classification result:', {
352
+ class: className,
353
+ confidence: confidence.toFixed(3),
354
+ classIndex: maxIdx
355
+ });
356
+ return {
357
+ class: className,
358
+ confidence: confidence,
359
+ classIndex: maxIdx
360
+ };
361
+ }
362
+ catch (error) {
363
+ this.debugLog('❌ Error classifying document:', error);
364
+ return null;
365
+ }
366
+ }
279
367
  cleanup() {
280
368
  if (this.animationId) {
281
369
  cancelAnimationFrame(this.animationId);
@@ -303,11 +391,16 @@ export class JaakStamps {
303
391
  this.captureCtx = undefined;
304
392
  this.captureCanvas = undefined;
305
393
  }
306
- // Disposed ONNX session
394
+ // Disposed ONNX sessions
307
395
  if (this.session) {
308
396
  this.session.release?.();
309
397
  this.session = undefined;
310
398
  }
399
+ if (this.mobileNetSession) {
400
+ this.mobileNetSession.release?.();
401
+ this.mobileNetSession = undefined;
402
+ }
403
+ this.mobileNetClassMap = undefined;
311
404
  this.isModelPreloaded = false;
312
405
  this.debugLog('🧹 Canvas pool cleaned up');
313
406
  }
@@ -438,18 +531,22 @@ export class JaakStamps {
438
531
  // Check if model is already preloaded
439
532
  if (!this.session) {
440
533
  this.isLoading = true;
441
- this.statusMessage = "Cargando modelo de detección...";
534
+ this.statusMessage = "Cargando modelos...";
442
535
  this.statusColor = "#007bff";
443
536
  const modelPath = this.MODEL_PATH;
444
- this.debugLog('🤖 Loading model:', modelPath);
537
+ this.debugLog('🤖 Loading detection model:', modelPath);
445
538
  const sessionOptions = this.getSessionOptions();
446
539
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
540
+ // Load MobileNet model if not already loaded
541
+ if (!this.mobileNetSession) {
542
+ await this.loadMobileNetModel();
543
+ }
447
544
  this.isModelPreloaded = true;
448
545
  this.emitReadyEvent();
449
546
  }
450
547
  else {
451
- this.debugLog('🚀 Using preloaded model');
452
- this.statusMessage = "Usando modelo precargado...";
548
+ this.debugLog('🚀 Using preloaded models');
549
+ this.statusMessage = "Usando modelos precargados...";
453
550
  this.statusColor = "#007bff";
454
551
  }
455
552
  this.statusMessage = "Configurando cámara...";
@@ -668,7 +765,9 @@ export class JaakStamps {
668
765
  corners?.forEach(corner => corner.classList.add('perfect-match'));
669
766
  if (!this.hasScreenshotTaken) {
670
767
  this.lastDetectedBox = bestBox;
671
- this.takeScreenshot();
768
+ this.takeScreenshot().catch(error => {
769
+ this.debugLog('❌ Error taking screenshot:', error);
770
+ });
672
771
  this.hasScreenshotTaken = true;
673
772
  // Reset para permitir segunda captura
674
773
  setTimeout(() => {
@@ -751,7 +850,7 @@ export class JaakStamps {
751
850
  ctx.strokeRect(x, y, w, h);
752
851
  });
753
852
  }
754
- takeScreenshot() {
853
+ async takeScreenshot() {
755
854
  if (!this.videoRef || !this.lastDetectedBox)
756
855
  return;
757
856
  // Activar animación
@@ -767,10 +866,10 @@ export class JaakStamps {
767
866
  // Calcular las coordenadas de recorte basadas en la detección
768
867
  const scaleX = this.videoRef.videoWidth / this.INPUT_SIZE;
769
868
  const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
770
- const cropX = Math.max(0, this.lastDetectedBox.x * scaleX);
771
- const cropY = Math.max(0, this.lastDetectedBox.y * scaleY);
772
- const cropWidth = Math.min(this.lastDetectedBox.w * scaleX, this.videoRef.videoWidth - cropX);
773
- const cropHeight = Math.min(this.lastDetectedBox.h * scaleY, this.videoRef.videoHeight - cropY);
869
+ const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
870
+ const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
871
+ const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
872
+ const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
774
873
  // OPTIMIZATION: Create temporary canvas only for cropped version
775
874
  // (We reuse main capture canvas for full frame)
776
875
  const croppedCanvas = document.createElement('canvas');
@@ -785,6 +884,15 @@ export class JaakStamps {
785
884
  // Captura del frente usando canvas reutilizado
786
885
  this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
787
886
  this.capturedCroppedId = croppedCanvas.toDataURL('image/png');
887
+ // Classify the cropped document
888
+ const classification = await this.classifyDocument(croppedCanvas);
889
+ if (classification && classification.class === 'passport') {
890
+ // If it's a passport, skip back capture since passports don't have a back side
891
+ this.debugLog('📄 Passport detected - skipping back capture');
892
+ this.completeProcess(true);
893
+ return;
894
+ }
895
+ // For other IDs, continue with normal flow (back capture)
788
896
  this.captureStep = 'back';
789
897
  this.statusMessage = "Voltee la identificación y muestre la parte trasera";
790
898
  this.statusColor = "#007bff";
@@ -914,7 +1022,7 @@ export class JaakStamps {
914
1022
  this.cleanup();
915
1023
  }
916
1024
  render() {
917
- 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" })))));
1025
+ 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" })))));
918
1026
  }
919
1027
  static get is() { return "jaak-stamps"; }
920
1028
  static get encapsulation() { return "shadow"; }
@@ -989,6 +1097,26 @@ export class JaakStamps {
989
1097
  "setter": false,
990
1098
  "reflect": false,
991
1099
  "defaultValue": "90"
1100
+ },
1101
+ "cropMargin": {
1102
+ "type": "number",
1103
+ "attribute": "crop-margin",
1104
+ "mutable": false,
1105
+ "complexType": {
1106
+ "original": "number",
1107
+ "resolved": "number",
1108
+ "references": {}
1109
+ },
1110
+ "required": false,
1111
+ "optional": false,
1112
+ "docs": {
1113
+ "tags": [],
1114
+ "text": ""
1115
+ },
1116
+ "getter": false,
1117
+ "setter": false,
1118
+ "reflect": false,
1119
+ "defaultValue": "0"
992
1120
  }
993
1121
  };
994
1122
  }