pipwave-ekyc-uikit 3.0.6-beta.2 → 3.0.6-beta.3
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.
- package/.env.development +3 -1
- package/.env.production +3 -1
- package/dist/pw-bundle.js +1 -1
- package/package.json +1 -1
package/.env.development
CHANGED
package/.env.production
CHANGED
package/dist/pw-bundle.js
CHANGED
|
@@ -4011,7 +4011,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
|
|
|
4011
4011
|
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
4012
4012
|
|
|
4013
4013
|
"use strict";
|
|
4014
|
-
eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst id_1 = __webpack_require__(/*! ../constant/id */ \"./src/constant/id.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst CARD_DETECTION_CONFIG = {\n // Model / inference\n // Fine-tuned YOLO11n, single \"document\" class, INT8. Output is [1, 5, N]:\n // cx, cy, w, h, score per anchor (N comes from the model, we don't hardcode it).\n MODEL_URL: '/card-models/document-detection-int8-480.onnx',\n // onnxruntime-web, served from our own origin (a Blob worker can't importScripts a\n // cross-origin URL). The matching .mjs/.wasm live next to it in public/ort/.\n ORT_URL: '/ort/ort.wasm.min.js',\n INPUT_SIZE: 480,\n MAX_THREADS: 4,\n // so cranking this up doesn't help and just hogs the CPU.\n // Detection rule\n // Two thresholds, not one, so a score wobbling around a single cutoff doesn't flip\n // valid/no-card every frame: needs to get above CONF_ENTER to count as a card, then\n // stays a card until it drops under CONF_EXIT.\n CONF_ENTER: 0.55,\n CONF_EXIT: 0.40,\n // Smooth the score so one bad frame doesn't drop us (the score tanks for a frame or two\n // on a held card when it blurs). ema = alpha*raw + (1-alpha)*ema; smaller alpha = smoother.\n CONF_EMA_ALPHA: 0.5,\n // Framing geometry (the real gate is containment: is the card inside the guide) ──\n MIN_FILL_RATIO: 0.45,\n // How far the card may poke past the guide (x min(guideW,guideH)) before it's \"out of box\".\n // POSITIVE = lenient (card can spill outside the guide). NEGATIVE = strict: the card must stay\n // this far INSIDE the guide, which shrinks the effective detection box so an angled/edging card\n // is flagged \"out-of-box\" early, before it actually crosses the visible guide line.\n CONTAINMENT_SLACK_RATIO: -0.04,\n // Loop / debounce\n // Asymmetric on purpose: slow to go green (don't want a false capture), quick to drop it.\n THROTTLE_FPS: 8,\n STABLE_FRAMES_TO_ENABLE: 2,\n STABLE_FRAMES_TO_DISABLE: 1,\n BORDER_STABLE_FRAMES: 2,\n BORDER_CLEAR_FRAMES: 1,\n INFERENCE_STUCK_MS: 3000, // if a frame's been out this long the worker lost it,\n // so clear the busy flag and carry on instead of hanging\n};\nconst ID_CARD_TYPES = [\n camera_1.CARD_TYPES.ID,\n camera_1.CARD_TYPES.ID_BACK,\n camera_1.CARD_TYPES.LICENSE,\n camera_1.CARD_TYPES.LICENSE_BACK,\n camera_1.CARD_TYPES.CC,\n camera_1.CARD_TYPES.CC_BACK,\n];\n// Inference worker\n// Runs onnxruntime-web off the main thread so WASM inference never blocks UI\n// paint. Receives a transferred ImageBitmap per frame, letterboxes + preprocesses\n// on an OffscreenCanvas, runs the model, and posts back the best box in video px.\n// Kept as a Blob string so it needs no webpack worker-loader config.\nconst WORKER_SOURCE = `\nlet session = null, ortLib = null, ready = false;\nlet INPUT = 320, ctx = null;\n// Input buffer + tensor, allocated once and reused every frame to avoid per-frame GC.\nlet inputBuf = null, inputTensor = null;\n\nself.onmessage = async (e) => {\n const msg = e.data;\n\n if (msg.type === 'init') {\n INPUT = msg.input;\n let stage = 'importScripts';\n try {\n importScripts(msg.ortUrl);\n stage = 'ort-global';\n ortLib = self.ort;\n if (!ortLib) throw new Error('self.ort is undefined after importScripts(' + msg.ortUrl + ')');\n stage = 'config';\n ortLib.env.wasm.wasmPaths = msg.wasmDir;\n ortLib.env.wasm.simd = true;\n // Threads need SharedArrayBuffer, which only exists if the page is cross-origin\n // isolated (COOP/COEP headers). No isolation, no threads - drop to one.\n ortLib.env.wasm.numThreads = self.crossOriginIsolated\n ? Math.min(msg.maxThreads || 4, navigator.hardwareConcurrency || 4)\n : 1;\n stage = 'offscreen-canvas';\n const canvas = new OffscreenCanvas(INPUT, INPUT);\n ctx = canvas.getContext('2d', { willReadFrequently: true });\n stage = 'session-create (' + msg.modelUrl + ')';\n session = await ortLib.InferenceSession.create(msg.modelUrl, {\n executionProviders: ['wasm'],\n graphOptimizationLevel: 'all',\n });\n ready = !!session;\n if (!ready) throw new Error('session create returned no session');\n // Allocate the input buffer/tensor once, then do one throwaway run. That first run\n // eats the graph-optimization + allocation cost so the first real frame isn't slow.\n stage = 'warmup';\n inputBuf = new Float32Array(3 * INPUT * INPUT);\n inputTensor = new ortLib.Tensor('float32', inputBuf, [1, 3, INPUT, INPUT]);\n try {\n await session.run({ [session.inputNames[0]]: inputTensor });\n } catch (_) { /* fine, the first real frame just won't have the warm-up */ }\n self.postMessage({\n type: 'ready', ok: true, ep: 'wasm',\n coi: !!self.crossOriginIsolated,\n threads: ortLib.env.wasm.numThreads,\n });\n return;\n } catch (err) {\n const detail = err && err.message ? err.message : String(err);\n self.postMessage({ type: 'ready', ok: false, error: '[' + stage + '] ' + detail });\n return;\n }\n }\n\n if (msg.type === 'frame') {\n const bitmap = msg.bitmap;\n if (!ready) { if (bitmap && bitmap.close) bitmap.close(); self.postMessage({ type: 'result', ok: false }); return; }\n\n const W = bitmap.width, H = bitmap.height, N = INPUT;\n const scale = Math.min(N / W, N / H);\n const newW = Math.round(W * scale), newH = Math.round(H * scale);\n const padX = (N - newW) / 2, padY = (N - newH) / 2;\n ctx.fillStyle = '#727272';\n ctx.fillRect(0, 0, N, N);\n ctx.drawImage(bitmap, 0, 0, W, H, padX, padY, newW, newH);\n if (bitmap.close) bitmap.close();\n\n const d = ctx.getImageData(0, 0, N, N).data;\n const area = N * N, f = inputBuf; // reused buffer; tensor wraps it by reference\n for (let i = 0; i < area; i++) {\n f[i] = d[i * 4] / 255;\n f[area + i] = d[i * 4 + 1] / 255;\n f[2 * area + i] = d[i * 4 + 2] / 255;\n }\n\n let out, inferMs = 0;\n try {\n const t0 = performance.now();\n out = await session.run({ [session.inputNames[0]]: inputTensor });\n inferMs = performance.now() - t0;\n } catch (err) { self.postMessage({ type: 'result', ok: false }); return; }\n\n const o = out[session.outputNames[0]], od = o.data, nA = o.dims[2];\n let bestScore = 0, bestIdx = -1; const sBase = 4 * nA;\n for (let a = 0; a < nA; a++) { const s = od[sBase + a]; if (s > bestScore) { bestScore = s; bestIdx = a; } }\n\n let box = null;\n if (bestIdx >= 0) {\n const cx = od[bestIdx], cy = od[nA + bestIdx], bw = od[2 * nA + bestIdx], bh = od[3 * nA + bestIdx];\n box = {\n x1: (cx - bw / 2 - padX) / scale, y1: (cy - bh / 2 - padY) / scale,\n x2: (cx + bw / 2 - padX) / scale, y2: (cy + bh / 2 - padY) / scale,\n };\n }\n self.postMessage({ type: 'result', ok: true, score: bestScore, box: box, W: W, H: H, inferMs: inferMs });\n }\n};\n`;\nclass CARDDETECTIONSDK {\n constructor(val) {\n this.type = camera_1.CARD_TYPES.ID;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n // Worker / model state\n this.worker = null;\n this.workerUrl = null;\n this.modelReady = false;\n this.modelLoadPromise = null;\n // Loop state\n this.rafId = null;\n this.inferenceBusy = false;\n this.lastFrameTime = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.stateChangeCallback = null;\n this.lastReason = '';\n this.lastInferMs = 0;\n this.cardPresent = false; // confidence-hysteresis latch (see _evaluateBox)\n this.scoreEma = 0; // smoothed confidence (rejects single-frame dropouts)\n this.inferenceStartedAt = 0; // timestamp of the in-flight frame (watchdog)\n Object.assign(this, val);\n }\n // Returns overlay dimensions in DISPLAY PIXELS — callers cropping from the\n // video stream must scale by videoWidth/clientWidth.\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || width;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || height;\n const idCardRatio = id_1.ID_CARD_RATIO[this.type] || id_1.ID_CARD_RATIO[\"others\"];\n const isDesktop = window.innerWidth >= id_1.VIEWPORT_SIZE.DESKTOP_BREAKPOINT;\n let newWidth;\n let newHeight;\n if (isDesktop) {\n const isIdCard = ID_CARD_TYPES.includes(this.type);\n const maxWidthRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_WIDTH_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_WIDTH_RATIO;\n const maxHeightRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_HEIGHT_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_HEIGHT_RATIO;\n const maxWidth = videoWidth * maxWidthRatio;\n const maxHeight = videoHeight * maxHeightRatio;\n const widthBasedHeight = maxWidth / idCardRatio;\n const heightBasedWidth = maxHeight * idCardRatio;\n if (widthBasedHeight <= maxHeight) {\n newWidth = maxWidth;\n newHeight = widthBasedHeight;\n }\n else {\n newWidth = heightBasedWidth;\n newHeight = maxHeight;\n }\n // prevent overlay from overlapping host page header\n if (newHeight > id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT) {\n newHeight = id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT;\n newWidth = newHeight * idCardRatio;\n }\n }\n else {\n const availableWidth = Math.min(videoWidth, window.innerWidth) -\n id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n newWidth = Math.max(availableWidth, 200); // min 200px for very small screens\n newHeight = newWidth / idCardRatio;\n const maxHeight = videoHeight - id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n if (maxHeight > 0 && newHeight > maxHeight) {\n newHeight = Math.max(maxHeight, 100);\n newWidth = newHeight * idCardRatio;\n }\n }\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d, _e, _f;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoWidth = this.videoElement.clientWidth;\n const videoHeight = this.videoElement.clientHeight;\n this.canvasElement.width = videoWidth;\n this.canvasElement.height = videoHeight;\n const { width, height } = this.calculateHeightNWidth(videoWidth, videoHeight);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"15px\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"0px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n const topBorder = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"inherit\");\n const bottomBorder = (_f = this.overlayElement) === null || _f === void 0 ? void 0 : _f.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"inherit\");\n }\n disableDetection() {\n var _a, _b;\n const topBorder = (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"none\");\n const bottomBorder = (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"none\");\n }\n init(type) {\n if (type)\n this.type = type;\n this.resizeElement();\n }\n // Spin up the inference worker and load the model inside it. Resolves even on\n // failure — the caller checks `modelReady` and degrades gracefully.\n loadModel() {\n if (this.modelLoadPromise)\n return this.modelLoadPromise;\n this.modelLoadPromise = new Promise((resolve) => {\n const supported = typeof Worker !== 'undefined' &&\n typeof OffscreenCanvas !== 'undefined' &&\n typeof createImageBitmap !== 'undefined';\n if (!supported) {\n console.warn('[card-detection] Worker/OffscreenCanvas unsupported — detection disabled');\n resolve();\n return;\n }\n try {\n const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });\n this.workerUrl = URL.createObjectURL(blob);\n this.worker = new Worker(this.workerUrl);\n }\n catch (e) {\n console.error('[card-detection] failed to start worker', e);\n resolve();\n return;\n }\n this.worker.onmessage = (e) => {\n const m = e.data;\n if ((m === null || m === void 0 ? void 0 : m.type) === 'ready') {\n this.modelReady = !!m.ok;\n if (m.ok) {\n if (MainPage_1.enableImageDebug)\n console.log(`[card-detection] model ready — backend=${m.ep} crossOriginIsolated=${m.coi} threads=${m.threads}`);\n }\n else {\n console.error('[card-detection] model failed to load in worker:', m.error || '(no detail)');\n }\n resolve();\n }\n else if ((m === null || m === void 0 ? void 0 : m.type) === 'result') {\n this._onResult(m);\n }\n };\n this.worker.onerror = (e) => {\n if (MainPage_1.enableImageDebug)\n console.error('[card-detection] worker error', e === null || e === void 0 ? void 0 : e.message);\n if (!this.modelReady)\n resolve();\n };\n // A Blob worker's base is blob:, so resolve every URL to absolute up front.\n const modelUrl = new URL(CARD_DETECTION_CONFIG.MODEL_URL, location.href).href;\n const ortUrl = new URL(CARD_DETECTION_CONFIG.ORT_URL, location.href).href;\n const wasmDir = ortUrl.replace(/[^/]*$/, ''); // dir holding the .mjs + .wasm\n this.worker.postMessage({\n type: 'init',\n input: CARD_DETECTION_CONFIG.INPUT_SIZE,\n maxThreads: CARD_DETECTION_CONFIG.MAX_THREADS,\n modelUrl,\n ortUrl,\n wasmDir,\n });\n });\n return this.modelLoadPromise;\n }\n startDetectionLoop(onStateChange) {\n return __awaiter(this, void 0, void 0, function* () {\n if (this.rafId)\n return;\n this.stateChangeCallback = onStateChange;\n yield this.modelLoadPromise;\n if (!this.modelReady) {\n onStateChange(true); // model/worker failed — let the user capture anyway\n return;\n }\n this.lastFrameTime = 0;\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.rafId = requestAnimationFrame((now) => this._loopTick(now));\n });\n }\n stopDetectionLoop() {\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.setBorderState('none');\n }\n // Terminal teardown — stops the loop and releases the worker. Call when the\n // camera is closed for good (card-camera `_cleanup`), not on pause.\n dispose() {\n this.stopDetectionLoop();\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n if (this.workerUrl) {\n URL.revokeObjectURL(this.workerUrl);\n this.workerUrl = null;\n }\n this.modelReady = false;\n this.modelLoadPromise = null;\n }\n setBorderState(state) {\n if (!this.overlayElement)\n return;\n const borders = [\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`),\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`),\n ];\n borders.forEach(el => {\n if (!el)\n return;\n el.classList.remove('border-element--valid', 'border-element--invalid');\n if (state !== 'none')\n el.classList.add(`border-element--${state}`);\n });\n }\n _loopTick(now) {\n var _a;\n this.rafId = requestAnimationFrame((n) => this._loopTick(n));\n if (now - this.lastFrameTime < 1000 / CARD_DETECTION_CONFIG.THROTTLE_FPS)\n return;\n // Watchdog: a frame out longer than INFERENCE_STUCK_MS means the worker died or\n // dropped it. Clear the busy flag so we recover - otherwise it stays true forever\n // and detection just quietly stops.\n if (this.inferenceBusy) {\n if (now - this.inferenceStartedAt < CARD_DETECTION_CONFIG.INFERENCE_STUCK_MS)\n return;\n if (MainPage_1.enableImageDebug)\n console.warn('[card-detection] inference watchdog fired, resetting stuck frame');\n this.inferenceBusy = false;\n }\n if (!this.worker || !this.modelReady)\n return;\n if (!((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.videoWidth))\n return;\n this.lastFrameTime = now;\n this.inferenceBusy = true;\n this.inferenceStartedAt = now;\n // Grab the frame as a transferable ImageBitmap, shrunk during decode to about\n // INPUT_SIZE on the long side. Sending the full 1280x960 every time and resizing\n // in the worker was wasteful. Resolution doesn't matter to detection - the guide is\n // normalised and the worker letterboxes whatever it gets.\n const N = CARD_DETECTION_CONFIG.INPUT_SIZE;\n const vw = this.videoElement.videoWidth, vh = this.videoElement.videoHeight;\n const s = N / Math.max(vw, vh);\n const rw = Math.max(1, Math.round(vw * s)), rh = Math.max(1, Math.round(vh * s));\n createImageBitmap(this.videoElement, { resizeWidth: rw, resizeHeight: rh, resizeQuality: 'low' })\n .then((bitmap) => {\n if (!this.worker) {\n bitmap.close();\n this.inferenceBusy = false;\n return;\n }\n this.worker.postMessage({ type: 'frame', bitmap }, [bitmap]);\n })\n .catch(() => { this.inferenceBusy = false; });\n }\n // Worker posted a detection result — do the (cheap) framing geometry against\n // the current guide on the main thread, then debounce.\n _onResult(m) {\n var _a, _b, _c, _d, _e;\n this.inferenceBusy = false;\n if (!m.ok)\n return;\n this.lastInferMs = (_a = m.inferMs) !== null && _a !== void 0 ? _a : 0;\n const state = this._evaluateBox((_b = m.score) !== null && _b !== void 0 ? _b : 0, (_c = m.box) !== null && _c !== void 0 ? _c : null, (_d = m.W) !== null && _d !== void 0 ? _d : 0, (_e = m.H) !== null && _e !== void 0 ? _e : 0);\n this._updateBorderDebounced(state);\n this._updateDebounce(state);\n }\n // Turns a raw detection into a state: below the confidence gate is 'no-card', a card\n // spilling out of the guide or too small for it is 'invalid', otherwise 'valid'.\n _evaluateBox(score, box, W, H) {\n // Smooth the score first so one dropout frame can't kick us out of the locked\n // state. A frame with no box counts as 0 and pulls the average down.\n const a = CARD_DETECTION_CONFIG.CONF_EMA_ALPHA;\n this.scoreEma = a * (box ? score : 0) + (1 - a) * this.scoreEma;\n const conf = this.scoreEma;\n // Hysteresis: once we've locked onto a card we let the score sag to CONF_EXIT before\n // giving up; until then it has to clear the higher CONF_ENTER. Stops the flicker.\n const gate = this.cardPresent ? CARD_DETECTION_CONFIG.CONF_EXIT : CARD_DETECTION_CONFIG.CONF_ENTER;\n if (!box || conf <= gate) {\n this.cardPresent = false;\n this._trace('no-card', conf, null);\n return 'no-card';\n }\n this.cardPresent = true;\n const guide = this._computeNormalizedGuide();\n if (!guide || !W || !H) {\n this._trace('VALID', conf, null);\n return 'valid';\n } // no guide → conf-only\n const gx1 = guide.left * W, gy1 = guide.top * H;\n const gx2 = (guide.left + guide.width) * W, gy2 = (guide.top + guide.height) * H;\n const gArea = Math.max(1, (gx2 - gx1) * (gy2 - gy1));\n const gMin = Math.min(gx2 - gx1, gy2 - gy1);\n // fill = how much of the guide the card covers (card too far → low fill).\n const inter = Math.max(0, Math.min(box.x2, gx2) - Math.max(box.x1, gx1))\n * Math.max(0, Math.min(box.y2, gy2) - Math.max(box.y1, gy1));\n const fill = inter / gArea;\n // containment = card stays inside the guide (+slack); spilling out → \"out of box\".\n const eps = CARD_DETECTION_CONFIG.CONTAINMENT_SLACK_RATIO * gMin;\n const contained = box.x1 >= gx1 - eps && box.y1 >= gy1 - eps\n && box.x2 <= gx2 + eps && box.y2 <= gy2 + eps;\n let result;\n let reason;\n if (!contained) {\n result = 'invalid';\n reason = 'out-of-box';\n }\n else if (fill < CARD_DETECTION_CONFIG.MIN_FILL_RATIO) {\n result = 'invalid';\n reason = 'too-small';\n }\n else {\n result = 'valid';\n reason = 'VALID';\n }\n this._trace(reason, conf, fill);\n return result;\n }\n // Guide rect as fractions of the raw (un-mirrored) frame - the space the worker's boxes\n // are in. Two gotchas to undo:\n // 1. the <video> is object-fit:cover, so it scales the frame to fill the element and\n // crops the rest. That means a fraction of what you see on screen isn't the same\n // fraction of the real frame. Skip this and a card poking into the cropped-off strip\n // looks \"inside the guide\" to the model. So we map display fractions back through\n // the cover crop.\n // 2. on desktop the video is mirrored (scaleX(-1)), so flip X back.\n _computeNormalizedGuide() {\n if (!this.overlayElement || !this.videoElement)\n return null;\n const oRect = this.overlayElement.getBoundingClientRect();\n const vRect = this.videoElement.getBoundingClientRect();\n if (!vRect.width || !vRect.height)\n return null;\n const VW = this.videoElement.videoWidth, VH = this.videoElement.videoHeight;\n if (!VW || !VH)\n return null;\n // Guide as fractions of the DISPLAYED video box.\n let left = (oRect.left - vRect.left) / vRect.width;\n let top = (oRect.top - vRect.top) / vRect.height;\n let width = oRect.width / vRect.width;\n let height = oRect.height / vRect.height;\n // Walk those display fractions back through the (centred) cover crop.\n const videoAspect = VW / VH, displayAspect = vRect.width / vRect.height;\n if (videoAspect >= displayAspect) {\n // frame is wider than the element, so the sides get cropped, full height shows\n const visW = displayAspect / videoAspect;\n left = (1 - visW) / 2 + left * visW;\n width = width * visW;\n }\n else {\n // frame is taller than the element, so top/bottom get cropped, full width shows\n const visH = videoAspect / displayAspect;\n top = (1 - visH) / 2 + top * visH;\n height = height * visH;\n }\n if (!(0, deviceDetection_1.isMobileDevice)())\n left = 1 - left - width;\n return { left, top, width, height };\n }\n // Diagnostic console trace. Only logs when the state actually changes, so no spam.\n _trace(reason, conf, fill) {\n if (reason === this.lastReason)\n return;\n this.lastReason = reason;\n if (MainPage_1.enableImageDebug)\n console.log(`[detect] ${reason} | conf=${conf.toFixed(3)} fill=${fill == null ? '-' : fill.toFixed(2)} (min ${CARD_DETECTION_CONFIG.MIN_FILL_RATIO}) gate=${CARD_DETECTION_CONFIG.CONF_ENTER}/${CARD_DETECTION_CONFIG.CONF_EXIT} | infer=${this.lastInferMs.toFixed(0)}ms`);\n }\n // Border colour debounce, deliberately lopsided: going green needs BORDER_STABLE_FRAMES\n // good frames in a row so we don't flash green by mistake, but dropping green only needs\n // BORDER_CLEAR_FRAMES, so pulling the card out clears it almost immediately.\n _updateBorderDebounced(rawState) {\n const target = rawState === 'valid' ? 'valid' :\n rawState === 'no-card' ? 'none' :\n 'invalid';\n if (target === this.candidateBorder) {\n this.candidateBorderCount++;\n }\n else {\n this.candidateBorder = target;\n this.candidateBorderCount = 1;\n }\n const needed = target === 'valid'\n ? CARD_DETECTION_CONFIG.BORDER_STABLE_FRAMES\n : CARD_DETECTION_CONFIG.BORDER_CLEAR_FRAMES;\n if (this.candidateBorderCount >= needed && target !== this.activeBorder) {\n this.activeBorder = target;\n this.setBorderState(target);\n }\n }\n _updateDebounce(rawState) {\n var _a;\n const wasFramed = this.isDocumentProperlyFramed;\n if (rawState === 'valid') {\n this.consecutiveInvalidFrames = 0;\n this.consecutiveValidFrames++;\n if (!this.isDocumentProperlyFramed &&\n this.consecutiveValidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_ENABLE) {\n this.isDocumentProperlyFramed = true;\n }\n }\n else {\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames++;\n if (this.isDocumentProperlyFramed &&\n this.consecutiveInvalidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_DISABLE) {\n this.isDocumentProperlyFramed = false;\n }\n }\n if (this.isDocumentProperlyFramed !== wasFramed) {\n (_a = this.stateChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.isDocumentProperlyFramed);\n }\n }\n}\nexports[\"default\"] = CARDDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-detection.ts?");
|
|
4014
|
+
eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst id_1 = __webpack_require__(/*! ../constant/id */ \"./src/constant/id.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst CARD_DETECTION_CONFIG = {\n // Model / inference\n // Fine-tuned YOLO11n, single \"document\" class, INT8. Output is [1, 5, N]:\n // cx, cy, w, h, score per anchor (N comes from the model, we don't hardcode it).\n // Loaded the same way as the face models: a base URL injected at build time from\n // .env (process.env.CARD_MODEL / ORT_PATH). Prod points at the assets CDN, dev at the\n // devServer's own /card-models + /ort. So consuming apps don't have to host anything.\n MODEL_URL: `${\"https://assets.pipwave.com/ekyc/sdk/card-models\" || 0}/document-detection-int8-480.onnx`,\n // onnxruntime-web. The matching .mjs/.wasm live next to it under ORT_PATH. A Blob worker\n // can't importScripts a cross-origin URL directly, so when ORT_PATH is on another origin\n // (the CDN) the worker fetches the script and importScripts it via a same-origin blob.\n ORT_URL: `${\"https://assets.pipwave.com/ekyc/sdk/ort\" || 0}/ort.wasm.min.js`,\n INPUT_SIZE: 480,\n MAX_THREADS: 4,\n // so cranking this up doesn't help and just hogs the CPU.\n // Detection rule\n // Two thresholds, not one, so a score wobbling around a single cutoff doesn't flip\n // valid/no-card every frame: needs to get above CONF_ENTER to count as a card, then\n // stays a card until it drops under CONF_EXIT.\n CONF_ENTER: 0.55,\n CONF_EXIT: 0.40,\n // Smooth the score so one bad frame doesn't drop us (the score tanks for a frame or two\n // on a held card when it blurs). ema = alpha*raw + (1-alpha)*ema; smaller alpha = smoother.\n CONF_EMA_ALPHA: 0.5,\n // Framing geometry (the real gate is containment: is the card inside the guide) ──\n MIN_FILL_RATIO: 0.45,\n // How far the card may poke past the guide (x min(guideW,guideH)) before it's \"out of box\".\n // POSITIVE = lenient (card can spill outside the guide). NEGATIVE = strict: the card must stay\n // this far INSIDE the guide, which shrinks the effective detection box so an angled/edging card\n // is flagged \"out-of-box\" early, before it actually crosses the visible guide line.\n CONTAINMENT_SLACK_RATIO: -0.04,\n // Loop / debounce\n // Asymmetric on purpose: slow to go green (don't want a false capture), quick to drop it.\n THROTTLE_FPS: 8,\n STABLE_FRAMES_TO_ENABLE: 2,\n STABLE_FRAMES_TO_DISABLE: 1,\n BORDER_STABLE_FRAMES: 2,\n BORDER_CLEAR_FRAMES: 1,\n INFERENCE_STUCK_MS: 3000, // if a frame's been out this long the worker lost it,\n // so clear the busy flag and carry on instead of hanging\n};\nconst ID_CARD_TYPES = [\n camera_1.CARD_TYPES.ID,\n camera_1.CARD_TYPES.ID_BACK,\n camera_1.CARD_TYPES.LICENSE,\n camera_1.CARD_TYPES.LICENSE_BACK,\n camera_1.CARD_TYPES.CC,\n camera_1.CARD_TYPES.CC_BACK,\n];\n// Inference worker\n// Runs onnxruntime-web off the main thread so WASM inference never blocks UI\n// paint. Receives a transferred ImageBitmap per frame, letterboxes + preprocesses\n// on an OffscreenCanvas, runs the model, and posts back the best box in video px.\n// Kept as a Blob string so it needs no webpack worker-loader config.\nconst WORKER_SOURCE = `\nlet session = null, ortLib = null, ready = false;\nlet INPUT = 320, ctx = null;\n// Input buffer + tensor, allocated once and reused every frame to avoid per-frame GC.\nlet inputBuf = null, inputTensor = null;\n\nself.onmessage = async (e) => {\n const msg = e.data;\n\n if (msg.type === 'init') {\n INPUT = msg.input;\n let stage = 'importScripts';\n try {\n // importScripts can't pull a cross-origin script into a blob worker. When ORT lives\n // on another origin (the CDN), fetch it (CORS) and import it via a same-origin blob.\n if (new URL(msg.ortUrl).origin === self.location.origin) {\n importScripts(msg.ortUrl);\n } else {\n const res = await fetch(msg.ortUrl, { mode: 'cors' });\n if (!res.ok) throw new Error('fetch ' + msg.ortUrl + ' -> ' + res.status);\n const blobUrl = URL.createObjectURL(await res.blob());\n importScripts(blobUrl);\n URL.revokeObjectURL(blobUrl);\n }\n stage = 'ort-global';\n ortLib = self.ort;\n if (!ortLib) throw new Error('self.ort is undefined after importScripts(' + msg.ortUrl + ')');\n stage = 'config';\n ortLib.env.wasm.wasmPaths = msg.wasmDir;\n ortLib.env.wasm.simd = true;\n // Threads need SharedArrayBuffer, which only exists if the page is cross-origin\n // isolated (COOP/COEP headers). No isolation, no threads - drop to one.\n ortLib.env.wasm.numThreads = self.crossOriginIsolated\n ? Math.min(msg.maxThreads || 4, navigator.hardwareConcurrency || 4)\n : 1;\n stage = 'offscreen-canvas';\n const canvas = new OffscreenCanvas(INPUT, INPUT);\n ctx = canvas.getContext('2d', { willReadFrequently: true });\n stage = 'session-create (' + msg.modelUrl + ')';\n session = await ortLib.InferenceSession.create(msg.modelUrl, {\n executionProviders: ['wasm'],\n graphOptimizationLevel: 'all',\n });\n ready = !!session;\n if (!ready) throw new Error('session create returned no session');\n // Allocate the input buffer/tensor once, then do one throwaway run. That first run\n // eats the graph-optimization + allocation cost so the first real frame isn't slow.\n stage = 'warmup';\n inputBuf = new Float32Array(3 * INPUT * INPUT);\n inputTensor = new ortLib.Tensor('float32', inputBuf, [1, 3, INPUT, INPUT]);\n try {\n await session.run({ [session.inputNames[0]]: inputTensor });\n } catch (_) { /* fine, the first real frame just won't have the warm-up */ }\n self.postMessage({\n type: 'ready', ok: true, ep: 'wasm',\n coi: !!self.crossOriginIsolated,\n threads: ortLib.env.wasm.numThreads,\n });\n return;\n } catch (err) {\n const detail = err && err.message ? err.message : String(err);\n self.postMessage({ type: 'ready', ok: false, error: '[' + stage + '] ' + detail });\n return;\n }\n }\n\n if (msg.type === 'frame') {\n const bitmap = msg.bitmap;\n if (!ready) { if (bitmap && bitmap.close) bitmap.close(); self.postMessage({ type: 'result', ok: false }); return; }\n\n const W = bitmap.width, H = bitmap.height, N = INPUT;\n const scale = Math.min(N / W, N / H);\n const newW = Math.round(W * scale), newH = Math.round(H * scale);\n const padX = (N - newW) / 2, padY = (N - newH) / 2;\n ctx.fillStyle = '#727272';\n ctx.fillRect(0, 0, N, N);\n ctx.drawImage(bitmap, 0, 0, W, H, padX, padY, newW, newH);\n if (bitmap.close) bitmap.close();\n\n const d = ctx.getImageData(0, 0, N, N).data;\n const area = N * N, f = inputBuf; // reused buffer; tensor wraps it by reference\n for (let i = 0; i < area; i++) {\n f[i] = d[i * 4] / 255;\n f[area + i] = d[i * 4 + 1] / 255;\n f[2 * area + i] = d[i * 4 + 2] / 255;\n }\n\n let out, inferMs = 0;\n try {\n const t0 = performance.now();\n out = await session.run({ [session.inputNames[0]]: inputTensor });\n inferMs = performance.now() - t0;\n } catch (err) { self.postMessage({ type: 'result', ok: false }); return; }\n\n const o = out[session.outputNames[0]], od = o.data, nA = o.dims[2];\n let bestScore = 0, bestIdx = -1; const sBase = 4 * nA;\n for (let a = 0; a < nA; a++) { const s = od[sBase + a]; if (s > bestScore) { bestScore = s; bestIdx = a; } }\n\n let box = null;\n if (bestIdx >= 0) {\n const cx = od[bestIdx], cy = od[nA + bestIdx], bw = od[2 * nA + bestIdx], bh = od[3 * nA + bestIdx];\n box = {\n x1: (cx - bw / 2 - padX) / scale, y1: (cy - bh / 2 - padY) / scale,\n x2: (cx + bw / 2 - padX) / scale, y2: (cy + bh / 2 - padY) / scale,\n };\n }\n self.postMessage({ type: 'result', ok: true, score: bestScore, box: box, W: W, H: H, inferMs: inferMs });\n }\n};\n`;\nclass CARDDETECTIONSDK {\n constructor(val) {\n this.type = camera_1.CARD_TYPES.ID;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n // Worker / model state\n this.worker = null;\n this.workerUrl = null;\n this.modelReady = false;\n this.modelLoadPromise = null;\n // Loop state\n this.rafId = null;\n this.inferenceBusy = false;\n this.lastFrameTime = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.stateChangeCallback = null;\n this.lastReason = '';\n this.lastInferMs = 0;\n this.cardPresent = false; // confidence-hysteresis latch (see _evaluateBox)\n this.scoreEma = 0; // smoothed confidence (rejects single-frame dropouts)\n this.inferenceStartedAt = 0; // timestamp of the in-flight frame (watchdog)\n Object.assign(this, val);\n }\n // Returns overlay dimensions in DISPLAY PIXELS — callers cropping from the\n // video stream must scale by videoWidth/clientWidth.\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || width;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || height;\n const idCardRatio = id_1.ID_CARD_RATIO[this.type] || id_1.ID_CARD_RATIO[\"others\"];\n const isDesktop = window.innerWidth >= id_1.VIEWPORT_SIZE.DESKTOP_BREAKPOINT;\n let newWidth;\n let newHeight;\n if (isDesktop) {\n const isIdCard = ID_CARD_TYPES.includes(this.type);\n const maxWidthRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_WIDTH_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_WIDTH_RATIO;\n const maxHeightRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_HEIGHT_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_HEIGHT_RATIO;\n const maxWidth = videoWidth * maxWidthRatio;\n const maxHeight = videoHeight * maxHeightRatio;\n const widthBasedHeight = maxWidth / idCardRatio;\n const heightBasedWidth = maxHeight * idCardRatio;\n if (widthBasedHeight <= maxHeight) {\n newWidth = maxWidth;\n newHeight = widthBasedHeight;\n }\n else {\n newWidth = heightBasedWidth;\n newHeight = maxHeight;\n }\n // prevent overlay from overlapping host page header\n if (newHeight > id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT) {\n newHeight = id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT;\n newWidth = newHeight * idCardRatio;\n }\n }\n else {\n const availableWidth = Math.min(videoWidth, window.innerWidth) -\n id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n newWidth = Math.max(availableWidth, 200); // min 200px for very small screens\n newHeight = newWidth / idCardRatio;\n const maxHeight = videoHeight - id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n if (maxHeight > 0 && newHeight > maxHeight) {\n newHeight = Math.max(maxHeight, 100);\n newWidth = newHeight * idCardRatio;\n }\n }\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d, _e, _f;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoWidth = this.videoElement.clientWidth;\n const videoHeight = this.videoElement.clientHeight;\n this.canvasElement.width = videoWidth;\n this.canvasElement.height = videoHeight;\n const { width, height } = this.calculateHeightNWidth(videoWidth, videoHeight);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"15px\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"0px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n const topBorder = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"inherit\");\n const bottomBorder = (_f = this.overlayElement) === null || _f === void 0 ? void 0 : _f.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"inherit\");\n }\n disableDetection() {\n var _a, _b;\n const topBorder = (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"none\");\n const bottomBorder = (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"none\");\n }\n init(type) {\n if (type)\n this.type = type;\n this.resizeElement();\n }\n // Spin up the inference worker and load the model inside it. Resolves even on\n // failure — the caller checks `modelReady` and degrades gracefully.\n loadModel() {\n if (this.modelLoadPromise)\n return this.modelLoadPromise;\n this.modelLoadPromise = new Promise((resolve) => {\n const supported = typeof Worker !== 'undefined' &&\n typeof OffscreenCanvas !== 'undefined' &&\n typeof createImageBitmap !== 'undefined';\n if (!supported) {\n console.warn('[card-detection] Worker/OffscreenCanvas unsupported — detection disabled');\n resolve();\n return;\n }\n try {\n const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });\n this.workerUrl = URL.createObjectURL(blob);\n this.worker = new Worker(this.workerUrl);\n }\n catch (e) {\n console.error('[card-detection] failed to start worker', e);\n resolve();\n return;\n }\n this.worker.onmessage = (e) => {\n const m = e.data;\n if ((m === null || m === void 0 ? void 0 : m.type) === 'ready') {\n this.modelReady = !!m.ok;\n if (m.ok) {\n if (MainPage_1.enableImageDebug)\n console.log(`[card-detection] model ready — backend=${m.ep} crossOriginIsolated=${m.coi} threads=${m.threads}`);\n }\n else {\n console.error('[card-detection] model failed to load in worker:', m.error || '(no detail)');\n }\n resolve();\n }\n else if ((m === null || m === void 0 ? void 0 : m.type) === 'result') {\n this._onResult(m);\n }\n };\n this.worker.onerror = (e) => {\n if (MainPage_1.enableImageDebug)\n console.error('[card-detection] worker error', e === null || e === void 0 ? void 0 : e.message);\n if (!this.modelReady)\n resolve();\n };\n // A Blob worker's base is blob:, so resolve every URL to absolute up front.\n const modelUrl = new URL(CARD_DETECTION_CONFIG.MODEL_URL, location.href).href;\n const ortUrl = new URL(CARD_DETECTION_CONFIG.ORT_URL, location.href).href;\n const wasmDir = ortUrl.replace(/[^/]*$/, ''); // dir holding the .mjs + .wasm\n this.worker.postMessage({\n type: 'init',\n input: CARD_DETECTION_CONFIG.INPUT_SIZE,\n maxThreads: CARD_DETECTION_CONFIG.MAX_THREADS,\n modelUrl,\n ortUrl,\n wasmDir,\n });\n });\n return this.modelLoadPromise;\n }\n startDetectionLoop(onStateChange) {\n return __awaiter(this, void 0, void 0, function* () {\n if (this.rafId)\n return;\n this.stateChangeCallback = onStateChange;\n yield this.modelLoadPromise;\n if (!this.modelReady) {\n onStateChange(true); // model/worker failed — let the user capture anyway\n return;\n }\n this.lastFrameTime = 0;\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.rafId = requestAnimationFrame((now) => this._loopTick(now));\n });\n }\n stopDetectionLoop() {\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.setBorderState('none');\n }\n // Terminal teardown — stops the loop and releases the worker. Call when the\n // camera is closed for good (card-camera `_cleanup`), not on pause.\n dispose() {\n this.stopDetectionLoop();\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n if (this.workerUrl) {\n URL.revokeObjectURL(this.workerUrl);\n this.workerUrl = null;\n }\n this.modelReady = false;\n this.modelLoadPromise = null;\n }\n setBorderState(state) {\n if (!this.overlayElement)\n return;\n const borders = [\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`),\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`),\n ];\n borders.forEach(el => {\n if (!el)\n return;\n el.classList.remove('border-element--valid', 'border-element--invalid');\n if (state !== 'none')\n el.classList.add(`border-element--${state}`);\n });\n }\n _loopTick(now) {\n var _a;\n this.rafId = requestAnimationFrame((n) => this._loopTick(n));\n if (now - this.lastFrameTime < 1000 / CARD_DETECTION_CONFIG.THROTTLE_FPS)\n return;\n // Watchdog: a frame out longer than INFERENCE_STUCK_MS means the worker died or\n // dropped it. Clear the busy flag so we recover - otherwise it stays true forever\n // and detection just quietly stops.\n if (this.inferenceBusy) {\n if (now - this.inferenceStartedAt < CARD_DETECTION_CONFIG.INFERENCE_STUCK_MS)\n return;\n if (MainPage_1.enableImageDebug)\n console.warn('[card-detection] inference watchdog fired, resetting stuck frame');\n this.inferenceBusy = false;\n }\n if (!this.worker || !this.modelReady)\n return;\n if (!((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.videoWidth))\n return;\n this.lastFrameTime = now;\n this.inferenceBusy = true;\n this.inferenceStartedAt = now;\n // Grab the frame as a transferable ImageBitmap, shrunk during decode to about\n // INPUT_SIZE on the long side. Sending the full 1280x960 every time and resizing\n // in the worker was wasteful. Resolution doesn't matter to detection - the guide is\n // normalised and the worker letterboxes whatever it gets.\n const N = CARD_DETECTION_CONFIG.INPUT_SIZE;\n const vw = this.videoElement.videoWidth, vh = this.videoElement.videoHeight;\n const s = N / Math.max(vw, vh);\n const rw = Math.max(1, Math.round(vw * s)), rh = Math.max(1, Math.round(vh * s));\n createImageBitmap(this.videoElement, { resizeWidth: rw, resizeHeight: rh, resizeQuality: 'low' })\n .then((bitmap) => {\n if (!this.worker) {\n bitmap.close();\n this.inferenceBusy = false;\n return;\n }\n this.worker.postMessage({ type: 'frame', bitmap }, [bitmap]);\n })\n .catch(() => { this.inferenceBusy = false; });\n }\n // Worker posted a detection result — do the (cheap) framing geometry against\n // the current guide on the main thread, then debounce.\n _onResult(m) {\n var _a, _b, _c, _d, _e;\n this.inferenceBusy = false;\n if (!m.ok)\n return;\n this.lastInferMs = (_a = m.inferMs) !== null && _a !== void 0 ? _a : 0;\n const state = this._evaluateBox((_b = m.score) !== null && _b !== void 0 ? _b : 0, (_c = m.box) !== null && _c !== void 0 ? _c : null, (_d = m.W) !== null && _d !== void 0 ? _d : 0, (_e = m.H) !== null && _e !== void 0 ? _e : 0);\n this._updateBorderDebounced(state);\n this._updateDebounce(state);\n }\n // Turns a raw detection into a state: below the confidence gate is 'no-card', a card\n // spilling out of the guide or too small for it is 'invalid', otherwise 'valid'.\n _evaluateBox(score, box, W, H) {\n // Smooth the score first so one dropout frame can't kick us out of the locked\n // state. A frame with no box counts as 0 and pulls the average down.\n const a = CARD_DETECTION_CONFIG.CONF_EMA_ALPHA;\n this.scoreEma = a * (box ? score : 0) + (1 - a) * this.scoreEma;\n const conf = this.scoreEma;\n // Hysteresis: once we've locked onto a card we let the score sag to CONF_EXIT before\n // giving up; until then it has to clear the higher CONF_ENTER. Stops the flicker.\n const gate = this.cardPresent ? CARD_DETECTION_CONFIG.CONF_EXIT : CARD_DETECTION_CONFIG.CONF_ENTER;\n if (!box || conf <= gate) {\n this.cardPresent = false;\n this._trace('no-card', conf, null);\n return 'no-card';\n }\n this.cardPresent = true;\n const guide = this._computeNormalizedGuide();\n if (!guide || !W || !H) {\n this._trace('VALID', conf, null);\n return 'valid';\n } // no guide → conf-only\n const gx1 = guide.left * W, gy1 = guide.top * H;\n const gx2 = (guide.left + guide.width) * W, gy2 = (guide.top + guide.height) * H;\n const gArea = Math.max(1, (gx2 - gx1) * (gy2 - gy1));\n const gMin = Math.min(gx2 - gx1, gy2 - gy1);\n // fill = how much of the guide the card covers (card too far → low fill).\n const inter = Math.max(0, Math.min(box.x2, gx2) - Math.max(box.x1, gx1))\n * Math.max(0, Math.min(box.y2, gy2) - Math.max(box.y1, gy1));\n const fill = inter / gArea;\n // containment = card stays inside the guide (+slack); spilling out → \"out of box\".\n const eps = CARD_DETECTION_CONFIG.CONTAINMENT_SLACK_RATIO * gMin;\n const contained = box.x1 >= gx1 - eps && box.y1 >= gy1 - eps\n && box.x2 <= gx2 + eps && box.y2 <= gy2 + eps;\n let result;\n let reason;\n if (!contained) {\n result = 'invalid';\n reason = 'out-of-box';\n }\n else if (fill < CARD_DETECTION_CONFIG.MIN_FILL_RATIO) {\n result = 'invalid';\n reason = 'too-small';\n }\n else {\n result = 'valid';\n reason = 'VALID';\n }\n this._trace(reason, conf, fill);\n return result;\n }\n // Guide rect as fractions of the raw (un-mirrored) frame - the space the worker's boxes\n // are in. Two gotchas to undo:\n // 1. the <video> is object-fit:cover, so it scales the frame to fill the element and\n // crops the rest. That means a fraction of what you see on screen isn't the same\n // fraction of the real frame. Skip this and a card poking into the cropped-off strip\n // looks \"inside the guide\" to the model. So we map display fractions back through\n // the cover crop.\n // 2. on desktop the video is mirrored (scaleX(-1)), so flip X back.\n _computeNormalizedGuide() {\n if (!this.overlayElement || !this.videoElement)\n return null;\n const oRect = this.overlayElement.getBoundingClientRect();\n const vRect = this.videoElement.getBoundingClientRect();\n if (!vRect.width || !vRect.height)\n return null;\n const VW = this.videoElement.videoWidth, VH = this.videoElement.videoHeight;\n if (!VW || !VH)\n return null;\n // Guide as fractions of the DISPLAYED video box.\n let left = (oRect.left - vRect.left) / vRect.width;\n let top = (oRect.top - vRect.top) / vRect.height;\n let width = oRect.width / vRect.width;\n let height = oRect.height / vRect.height;\n // Walk those display fractions back through the (centred) cover crop.\n const videoAspect = VW / VH, displayAspect = vRect.width / vRect.height;\n if (videoAspect >= displayAspect) {\n // frame is wider than the element, so the sides get cropped, full height shows\n const visW = displayAspect / videoAspect;\n left = (1 - visW) / 2 + left * visW;\n width = width * visW;\n }\n else {\n // frame is taller than the element, so top/bottom get cropped, full width shows\n const visH = videoAspect / displayAspect;\n top = (1 - visH) / 2 + top * visH;\n height = height * visH;\n }\n if (!(0, deviceDetection_1.isMobileDevice)())\n left = 1 - left - width;\n return { left, top, width, height };\n }\n // Diagnostic console trace. Only logs when the state actually changes, so no spam.\n _trace(reason, conf, fill) {\n if (reason === this.lastReason)\n return;\n this.lastReason = reason;\n if (MainPage_1.enableImageDebug)\n console.log(`[detect] ${reason} | conf=${conf.toFixed(3)} fill=${fill == null ? '-' : fill.toFixed(2)} (min ${CARD_DETECTION_CONFIG.MIN_FILL_RATIO}) gate=${CARD_DETECTION_CONFIG.CONF_ENTER}/${CARD_DETECTION_CONFIG.CONF_EXIT} | infer=${this.lastInferMs.toFixed(0)}ms`);\n }\n // Border colour debounce, deliberately lopsided: going green needs BORDER_STABLE_FRAMES\n // good frames in a row so we don't flash green by mistake, but dropping green only needs\n // BORDER_CLEAR_FRAMES, so pulling the card out clears it almost immediately.\n _updateBorderDebounced(rawState) {\n const target = rawState === 'valid' ? 'valid' :\n rawState === 'no-card' ? 'none' :\n 'invalid';\n if (target === this.candidateBorder) {\n this.candidateBorderCount++;\n }\n else {\n this.candidateBorder = target;\n this.candidateBorderCount = 1;\n }\n const needed = target === 'valid'\n ? CARD_DETECTION_CONFIG.BORDER_STABLE_FRAMES\n : CARD_DETECTION_CONFIG.BORDER_CLEAR_FRAMES;\n if (this.candidateBorderCount >= needed && target !== this.activeBorder) {\n this.activeBorder = target;\n this.setBorderState(target);\n }\n }\n _updateDebounce(rawState) {\n var _a;\n const wasFramed = this.isDocumentProperlyFramed;\n if (rawState === 'valid') {\n this.consecutiveInvalidFrames = 0;\n this.consecutiveValidFrames++;\n if (!this.isDocumentProperlyFramed &&\n this.consecutiveValidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_ENABLE) {\n this.isDocumentProperlyFramed = true;\n }\n }\n else {\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames++;\n if (this.isDocumentProperlyFramed &&\n this.consecutiveInvalidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_DISABLE) {\n this.isDocumentProperlyFramed = false;\n }\n }\n if (this.isDocumentProperlyFramed !== wasFramed) {\n (_a = this.stateChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.isDocumentProperlyFramed);\n }\n }\n}\nexports[\"default\"] = CARDDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-detection.ts?");
|
|
4015
4015
|
|
|
4016
4016
|
/***/ }),
|
|
4017
4017
|
|