@beltec/face-gate 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/react/FaceGate.tsx","../src/core/types.ts","../src/core/stream.ts","../src/react/Emblem.tsx","../src/react/framing.ts","../src/react/labels.ts"],"sourcesContent":["/**\r\n * @beltec/face-gate — Face Gate, uma solução BelTec.\r\n *\r\n * Navegador: <FaceGate> (webcam com validação em tempo real) e o cliente do protocolo.\r\n * Servidor: importe de \"@beltec/face-gate/server\" (guarda o client_secret; nunca no navegador).\r\n * Estilos: import \"@beltec/face-gate/styles.css\".\r\n */\r\nexport { FaceGate, type FaceGateProps, type FaceGateCapture } from \"./react/FaceGate\";\r\nexport { Emblem } from \"./react/Emblem\";\r\nexport { DEFAULT_LABELS, FAIL_LABELS, describeFails, type FaceGateLabels } from \"./react/labels\";\r\nexport { openFaceGateStream, type FaceGateStream, type StreamHandlers } from \"./core/stream\";\r\nexport * from \"./core/types\";\r\n","import { useEffect, useRef, useState, type CSSProperties } from \"react\";\r\nimport { openFaceGateStream, type FaceGateStream } from \"../core/stream\";\r\nimport { FaceGateError, type FaceBox, type Stable, type StreamOptions, type StreamTicket, type ValidationResult } from \"../core/types\";\r\nimport { Emblem } from \"./Emblem\";\r\nimport {\r\n FULL,\r\n WHOLE,\r\n aimCrop,\r\n areaFromPixels,\r\n baseCrop,\r\n boxStyle,\r\n cropTransform,\r\n ease,\r\n fitCrop,\r\n framingThresholds,\r\n isDark,\r\n moved,\r\n overlayTransform,\r\n type Area,\r\n type Crop,\r\n} from \"./framing\";\r\nimport { DEFAULT_LABELS, describeFails, type FaceGateLabels } from \"./labels\";\r\n\r\nexport type FaceGateCapture = {\r\n /**\r\n * Foto final: sem fundo (PNG) quando `clean`; senão a melhorada (JPEG) quando `enhance`; senão o quadro\r\n * capturado (JPEG). Data URL ou blob URL.\r\n */\r\n image: string;\r\n /** O quadro capturado, como enviado à API. */\r\n frame: Blob;\r\n result: ValidationResult;\r\n};\r\n\r\nexport type FaceGateProps = StreamOptions & {\r\n /** Pede ao SEU servidor um ticket de uso único (ex.: fetch(\"/api/face-gate/ticket\", { method: \"POST\" })). */\r\n getTicket: () => Promise<StreamTicket>;\r\n /** Foto capturada e validada (aprovada ou reprovada na validação final). */\r\n onCapture?: (capture: FaceGateCapture) => void;\r\n /** Cada quadro analisado ao vivo. */\r\n onLive?: (result: ValidationResult) => void;\r\n onError?: (error: FaceGateError) => void;\r\n /** Liga a câmera ao montar (padrão: espera o clique em \"Ligar câmera\"). */\r\n autoStart?: boolean;\r\n /** Abertura animada do escudo ao ligar a câmera (padrão: true; desligada com prefers-reduced-motion). */\r\n intro?: boolean;\r\n /** Marca d'água do escudo no canto do vídeo (padrão: true). */\r\n watermark?: boolean;\r\n /**\r\n * Enquadramento automático (padrão: true): a imagem aproxima e acompanha o rosto, mantendo-o no centro; a foto\r\n * capturada é esse recorte e só sai com o rosto centralizado (tolerância de 8%). Ignora as tarjas pretas que\r\n * algumas câmeras acrescentam.\r\n */\r\n autoFrame?: boolean;\r\n /** Prévia espelhada, como um espelho (padrão: true). A foto enviada e a final não são espelhadas. */\r\n mirror?: boolean;\r\n labels?: Partial<FaceGateLabels>;\r\n className?: string;\r\n style?: CSSProperties;\r\n};\r\n\r\ntype Phase = \"off\" | \"starting\" | \"live\" | \"processing\" | \"done\";\r\nconst NO_STABLE: Stable = { ms: 0, needed: 0 };\r\nconst CENTER: FaceBox = { xmin: 0.3, ymin: 0.2, xmax: 0.7, ymax: 0.8 };\r\nconst INTRO_MS = 3400;\r\nconst KEEP_FRAMES = 15;\r\n\r\n/** Sem rosto por esse tempo: o recorte volta para a imagem inteira (área útil). */\r\nconst LOST_MS = 1500;\r\n/** Maior lado do quadro enviado (sem ampliar o recorte além da resolução da câmera). */\r\nconst SEND_MAX = 800;\r\n/** Quadros entre medições da área útil (a 1ª é logo no início). */\r\nconst MEASURE_EVERY = 30;\r\n/** Câmera em resolução maior para o recorte; se vierem quadros pretos (alguns Android), reabre na simples. */\r\nconst CAMERA_HD: MediaTrackConstraints = { width: { ideal: 1280 }, height: { ideal: 960 }, facingMode: \"user\" };\r\nconst CAMERA_SD: MediaTrackConstraints = { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: \"user\" };\r\nconst DARK_FRAMES = 3;\r\n\r\n/**\r\n * Webcam com validação em tempo real do Face Gate: uma conexão WebSocket por sessão, mira que segue o\r\n * rosto, captura automática só depois de ~0,8 s aprovado e parado, scanner sobre a foto até a resposta.\r\n * Importe também \"@beltec/face-gate/styles.css\".\r\n */\r\nexport function FaceGate({\r\n getTicket, onCapture, onLive, onError, autoStart = false, intro = true, watermark = true, labels, className,\r\n style, clean = false, model, device, thresholds, enhance = null, autoFrame = true, mirror = true,\r\n}: FaceGateProps) {\r\n const t = { ...DEFAULT_LABELS, ...labels };\r\n const videoRef = useRef<HTMLVideoElement>(null);\r\n const mediaRef = useRef<MediaStream | null>(null);\r\n const streamRef = useRef<FaceGateStream | null>(null);\r\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\r\n const framesRef = useRef(new Map<number, { blob: Blob; crop: Crop; result?: ValidationResult }>());\r\n // enquadramento: recorte atual (suavizado; o mesmo da prévia e do envio), alvo e área útil da imagem\r\n const viewRef = useRef<HTMLDivElement>(null);\r\n // camada da moldura ao vivo e o recorte do quadro cujo resultado ela mostra (ver overlayTransform)\r\n const overlayRef = useRef<HTMLDivElement>(null);\r\n const overlayCropRef = useRef<Crop>(FULL);\r\n // ritmo de envio dos quadros (o max_fps que a API informa no \"ready\") e o seq do último resultado mostrado\r\n const fpsRef = useRef(6);\r\n const sendRef = useRef<() => void>(() => {});\r\n const lastSeqRef = useRef(0);\r\n const cropRef = useRef<Crop>(FULL);\r\n const targetRef = useRef<Crop>(FULL);\r\n const areaRef = useRef<Area>(WHOLE);\r\n const measuredRef = useRef(0);\r\n const areaCanvasRef = useRef<HTMLCanvasElement | null>(null);\r\n const lostSinceRef = useRef(0);\r\n const darkRef = useRef({ count: 0, checked: 0, fellBack: false });\r\n const capturedRef = useRef<{ blob: Blob; url: string } | null>(null);\r\n const gateRef = useRef({ camera: false, socket: false, intro: false });\r\n const hadFaceRef = useRef(false);\r\n const introTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n const [phase, setPhase] = useState<Phase>(\"off\");\r\n const [showIntro, setShowIntro] = useState(false);\r\n const [live, setLive] = useState<ValidationResult | null>(null);\r\n const [stable, setStable] = useState<Stable>(NO_STABLE);\r\n const [detectKey, setDetectKey] = useState(0);\r\n const [frozen, setFrozen] = useState<{ url: string; box: FaceBox } | null>(null);\r\n const [final, setFinal] = useState<{ url: string; result: ValidationResult } | null>(null);\r\n const [error, setError] = useState<string | null>(null);\r\n // proporção real da câmera (no celular ela vem em pé, 3:4): o quadro acompanha\r\n const [ratio, setRatio] = useState(4 / 3);\r\n\r\n // Valores mais recentes em ref: a sessão não reinicia a cada render. Com o enquadramento, o centro é exigido.\r\n const th = autoFrame ? framingThresholds(thresholds) : thresholds;\r\n const cfg = useRef({ getTicket, onCapture, onLive, onError, options: { clean, model, device, thresholds: th, enhance } });\r\n useEffect(() => {\r\n cfg.current = { getTicket, onCapture, onLive, onError, options: { clean, model, device, thresholds: th, enhance } };\r\n });\r\n useEffect(() => {\r\n streamRef.current?.configure({ clean, model, device, thresholds: th, enhance });\r\n // th é derivado de thresholds/autoFrame\r\n }, [clean, model, device, thresholds, autoFrame, enhance]);\r\n\r\n // Recorte em movimento suave na prévia enquanto a câmera está ao vivo (direto no DOM, sem re-render).\r\n useEffect(() => {\r\n if (phase !== \"live\" || !autoFrame) return;\r\n let id = 0;\r\n const tick = () => {\r\n cropRef.current = ease(cropRef.current, targetRef.current);\r\n if (viewRef.current) viewRef.current.style.transform = cropTransform(cropRef.current, mirror);\r\n if (overlayRef.current) overlayRef.current.style.transform = overlayTransform(overlayCropRef.current, cropRef.current, mirror);\r\n id = requestAnimationFrame(tick);\r\n };\r\n id = requestAnimationFrame(tick);\r\n return () => cancelAnimationFrame(id);\r\n }, [phase, autoFrame, mirror]);\r\n\r\n function release() {\r\n if (introTimer.current) clearTimeout(introTimer.current);\r\n streamRef.current?.close();\r\n streamRef.current = null;\r\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\r\n mediaRef.current = null;\r\n }\r\n useEffect(() => release, []);\r\n\r\n function fail(err: FaceGateError) {\r\n release();\r\n setShowIntro(false);\r\n setPhase(\"off\");\r\n setError(err.message);\r\n cfg.current.onError?.(err);\r\n }\r\n\r\n /** Mede a área útil (sem tarjas pretas) numa miniatura do quadro inteiro; recorte e alvo ficam dentro dela. */\r\n function measureArea(v: HTMLVideoElement) {\r\n if (measuredRef.current++ % MEASURE_EVERY !== 0) return;\r\n const W = 64;\r\n const H = Math.max(8, Math.round((W * v.videoHeight) / v.videoWidth));\r\n const c = (areaCanvasRef.current ??= document.createElement(\"canvas\"));\r\n c.width = W;\r\n c.height = H;\r\n const ctx = c.getContext(\"2d\", { willReadFrequently: true });\r\n if (!ctx) return;\r\n ctx.drawImage(v, 0, 0, W, H);\r\n const next = areaFromPixels(ctx.getImageData(0, 0, W, H).data, W, H);\r\n const prev = areaRef.current;\r\n if (Math.abs(next.x - prev.x) + Math.abs(next.y - prev.y) + Math.abs(next.w - prev.w) + Math.abs(next.h - prev.h) < 0.02) return;\r\n areaRef.current = next;\r\n const into = (cr: Crop) => fitCrop(cr.s >= 0.999 ? baseCrop(next) : cr, next);\r\n cropRef.current = into(cropRef.current);\r\n targetRef.current = into(targetRef.current);\r\n }\r\n\r\n /** Nos primeiros quadros: preto seguido = câmera que não entrega imagem em HD; reabre em SD, uma vez. */\r\n function checkDark(ctx: CanvasRenderingContext2D, w: number, h: number): boolean {\r\n const d = darkRef.current;\r\n if (d.fellBack || d.checked >= DARK_FRAMES * 2) return false;\r\n d.checked += 1;\r\n d.count = isDark(ctx.getImageData(0, 0, w, h).data) ? d.count + 1 : 0;\r\n if (d.count < DARK_FRAMES) return false;\r\n d.fellBack = true;\r\n void reopenCamera();\r\n return true;\r\n }\r\n\r\n async function reopenCamera() {\r\n try {\r\n const media = await navigator.mediaDevices.getUserMedia({ video: CAMERA_SD });\r\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\r\n mediaRef.current = media;\r\n if (videoRef.current) {\r\n videoRef.current.srcObject = media;\r\n await videoRef.current.play().catch(() => {});\r\n }\r\n } catch {\r\n // segue com a câmera que já está aberta\r\n }\r\n requestAnimationFrame(sendFrame);\r\n }\r\n\r\n function aim(seq: number, box: FaceBox | null | undefined) {\r\n if (!autoFrame) return;\r\n const now = performance.now();\r\n if (!box) {\r\n lostSinceRef.current ||= now;\r\n if (now - lostSinceRef.current > LOST_MS) targetRef.current = baseCrop(areaRef.current);\r\n return;\r\n }\r\n lostSinceRef.current = 0;\r\n const used = framesRef.current.get(seq)?.crop ?? cropRef.current;\r\n const next = aimCrop(box, used, areaRef.current);\r\n if (moved(next, targetRef.current)) targetRef.current = next;\r\n }\r\n\r\n function sendFrame() {\r\n const v = videoRef.current;\r\n const s = streamRef.current;\r\n if (!v || !v.videoWidth || !s) return;\r\n if (autoFrame) measureArea(v);\r\n const crop = autoFrame ? cropRef.current : FULL;\r\n const sw = crop.s * v.videoWidth;\r\n const sh = crop.s * v.videoHeight;\r\n const k = Math.min(1, SEND_MAX / Math.max(sw, sh));\r\n const c = (canvasRef.current ??= document.createElement(\"canvas\"));\r\n c.width = Math.round(sw * k);\r\n c.height = Math.round(sh * k);\r\n const ctx = c.getContext(\"2d\", { willReadFrequently: true });\r\n ctx?.drawImage(v, crop.x * v.videoWidth, crop.y * v.videoHeight, sw, sh, 0, 0, c.width, c.height);\r\n if (autoFrame && ctx && checkDark(ctx, c.width, c.height)) return; // reabrindo a câmera em SD\r\n c.toBlob(\r\n (blob) => {\r\n const cur = streamRef.current;\r\n if (!blob || !cur) return;\r\n const seq = cur.sendFrame(blob);\r\n if (seq == null) return;\r\n const frames = framesRef.current;\r\n frames.set(seq, { blob, crop });\r\n for (const k of frames.keys()) if (k <= seq - KEEP_FRAMES) frames.delete(k);\r\n },\r\n \"image/jpeg\",\r\n 0.85,\r\n );\r\n }\r\n\r\n function goLiveIfReady() {\r\n const g = gateRef.current;\r\n if (!g.camera || !g.socket || !g.intro || !streamRef.current) return;\r\n setShowIntro(false);\r\n setPhase(\"live\");\r\n sendFrame();\r\n }\r\n\r\n async function start() {\r\n setError(null);\r\n if (typeof window === \"undefined\") return;\r\n if (!window.isSecureContext) return fail(new FaceGateError(\"camera_unavailable\", t.insecure));\r\n if (!navigator.mediaDevices?.getUserMedia) return fail(new FaceGateError(\"camera_unavailable\", t.noCamera));\r\n release();\r\n const reduceMotion = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\r\n const playIntro = intro && !reduceMotion;\r\n gateRef.current = { camera: false, socket: false, intro: !playIntro };\r\n framesRef.current.clear();\r\n hadFaceRef.current = false;\r\n lastSeqRef.current = 0;\r\n cropRef.current = targetRef.current = overlayCropRef.current = FULL;\r\n if (overlayRef.current) overlayRef.current.style.transform = \"\";\r\n areaRef.current = WHOLE;\r\n measuredRef.current = 0;\r\n lostSinceRef.current = 0;\r\n darkRef.current = { count: 0, checked: 0, fellBack: false };\r\n if (viewRef.current) viewRef.current.style.transform = cropTransform(FULL, mirror);\r\n setLive(null);\r\n setStable(NO_STABLE);\r\n setFrozen(null);\r\n setFinal(null);\r\n setShowIntro(playIntro);\r\n setPhase(\"starting\");\r\n if (playIntro) {\r\n introTimer.current = setTimeout(() => {\r\n gateRef.current.intro = true;\r\n goLiveIfReady();\r\n }, INTRO_MS);\r\n }\r\n\r\n const [cam, tk] = await Promise.allSettled([\r\n navigator.mediaDevices.getUserMedia({ video: autoFrame ? CAMERA_HD : CAMERA_SD }),\r\n cfg.current.getTicket(),\r\n ]);\r\n if (cam.status === \"rejected\") {\r\n const name = (cam.reason as DOMException)?.name;\r\n const msg = name === \"NotAllowedError\" ? t.denied : name === \"NotFoundError\" ? t.notFound : name === \"NotReadableError\" ? t.busy : String((cam.reason as Error)?.message ?? cam.reason);\r\n return fail(new FaceGateError(name === \"NotAllowedError\" ? \"camera_denied\" : \"camera_unavailable\", msg));\r\n }\r\n if (tk.status === \"rejected\") {\r\n cam.value.getTracks().forEach((tr) => tr.stop());\r\n const e = tk.reason;\r\n return fail(e instanceof FaceGateError ? e : new FaceGateError(\"network\", (e as Error)?.message ?? \"Falha ao obter o ticket\"));\r\n }\r\n\r\n mediaRef.current = cam.value;\r\n if (videoRef.current) {\r\n videoRef.current.srcObject = cam.value;\r\n void videoRef.current.play().catch(() => {});\r\n }\r\n streamRef.current = openFaceGateStream(tk.value.url, tk.value.ticket, cfg.current.options, {\r\n onReady: (maxFps) => {\r\n fpsRef.current = Math.min(Math.max(maxFps, 1), 10);\r\n gateRef.current.socket = true;\r\n goLiveIfReady();\r\n },\r\n onFrame: (seq, r, st) => {\r\n if (seq <= lastSeqRef.current) return; // resposta atrasada de um quadro mais velho que o mostrado\r\n lastSeqRef.current = seq;\r\n const f = framesRef.current.get(seq);\r\n if (f) f.result = r;\r\n if (autoFrame) overlayCropRef.current = f?.crop ?? cropRef.current;\r\n aim(seq, r.dominant_face_bbox);\r\n const hasFace = Boolean(r.dominant_face_bbox);\r\n if (hasFace && !hadFaceRef.current) setDetectKey((k) => k + 1);\r\n hadFaceRef.current = hasFace;\r\n setLive(r);\r\n setStable(st);\r\n cfg.current.onLive?.(r);\r\n // os quadros seguem saindo no ritmo fixo (efeito abaixo); a API só captura quando estável\r\n },\r\n onCaptured: (seq) => {\r\n const frames = framesRef.current;\r\n const pick = frames.get(seq) ?? frames.get(Math.max(...frames.keys()));\r\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\r\n mediaRef.current = null;\r\n if (pick) {\r\n const url = URL.createObjectURL(pick.blob);\r\n capturedRef.current = { blob: pick.blob, url };\r\n setFrozen({ url, box: pick.result?.dominant_face_bbox ?? CENTER });\r\n }\r\n setPhase(\"processing\");\r\n },\r\n onFinal: (_seq, r) => {\r\n streamRef.current?.close();\r\n streamRef.current = null;\r\n const cap = capturedRef.current;\r\n const image = r.cleaned_image ?? r.enhancement?.enhanced_image ?? cap?.url ?? \"\";\r\n setFinal({ url: image, result: r });\r\n setPhase(\"done\");\r\n if (cap) cfg.current.onCapture?.({ image, frame: cap.blob, result: r });\r\n },\r\n onError: fail,\r\n });\r\n }\r\n\r\n function stop() {\r\n release();\r\n setShowIntro(false);\r\n setPhase(\"off\");\r\n }\r\n\r\n useEffect(() => {\r\n if (autoStart) void start();\r\n // só ao montar\r\n }, []);\r\n\r\n // Quadros num ritmo fixo enquanto ao vivo (não esperam o resultado do anterior: a moldura atualiza mais rápido).\r\n useEffect(() => {\r\n sendRef.current = sendFrame;\r\n });\r\n useEffect(() => {\r\n if (phase !== \"live\") return;\r\n const id = setInterval(() => sendRef.current(), 1000 / fpsRef.current);\r\n return () => clearInterval(id);\r\n }, [phase]);\r\n\r\n const cameraOn = phase === \"starting\" || phase === \"live\";\r\n const box = phase === \"live\" ? (live?.dominant_face_bbox ?? null) : null;\r\n const ok = Boolean(live?.accepted);\r\n const holding = phase === \"live\" && ok && stable.ms > 0 && stable.needed > 0;\r\n const progress = holding ? Math.min(stable.ms / stable.needed, 1) : 0;\r\n const status =\r\n phase === \"starting\" ? t.connecting\r\n : !live ? t.searching\r\n : live.accepted ? (holding ? t.hold : t.allGood)\r\n : !live.dominant_face_bbox ? t.searching\r\n : `${t.fix}: ${describeFails(live.hard_fails)}`;\r\n\r\n return (\r\n <div className={[\"fg-root\", className].filter(Boolean).join(\" \")} style={style}>\r\n {/* proporção da câmera, sem passar de ~70% da altura da tela; geometria no estilo (não depende do CSS) */}\r\n <div className=\"fg-stage\" style={{ aspectRatio: String(ratio), width: `min(100%, calc(70dvh * ${ratio}))`, marginInline: \"auto\" }}>\r\n {/* camada com o recorte e o espelho: o <video> fica sem transformação (em alguns Android ele fica preto) */}\r\n <div ref={viewRef} hidden={!cameraOn}\r\n style={{ position: \"absolute\", inset: 0, transformOrigin: \"0 0\", transform: cropTransform(FULL, mirror) }}>\r\n <video ref={videoRef} autoPlay playsInline muted className=\"fg-video\"\r\n onLoadedData={(e) => {\r\n const v = e.currentTarget;\r\n if (v.videoWidth && v.videoHeight) setRatio(v.videoWidth / v.videoHeight);\r\n gateRef.current.camera = true;\r\n goLiveIfReady();\r\n }} />\r\n </div>\r\n\r\n {phase === \"live\" && !box && (\r\n <div className=\"fg-search\" aria-hidden=\"true\"\r\n style={{ top: \"15%\", left: \"50%\", width: \"auto\", height: \"68%\", aspectRatio: \"3 / 4\", maxWidth: \"80%\", translate: \"-50% 0\" }}>\r\n <span className=\"fg-search-sweep\" />\r\n </div>\r\n )}\r\n {/* a moldura vem nas coordenadas do recorte do quadro analisado; esta camada a leva para o recorte da tela */}\r\n <div ref={overlayRef} aria-hidden=\"true\"\r\n style={{ position: \"absolute\", inset: 0, transformOrigin: \"0 0\", pointerEvents: \"none\" }}>\r\n {box && (\r\n <div key={detectKey} className=\"fg-track\" data-state={ok ? \"ok\" : \"warn\"} style={boxStyle(box, mirror)}>\r\n <span className=\"fg-track-line\" />\r\n {holding && <span className=\"fg-track-hold\" style={{ transform: `scaleX(${progress})` }} />}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {phase === \"processing\" && frozen && (\r\n <>\r\n <img src={frozen.url} alt=\"\" className=\"fg-video\" style={mirror ? { transform: \"scaleX(-1)\" } : undefined} />\r\n <div className=\"fg-scan\" style={boxStyle(frozen.box, mirror)} aria-hidden=\"true\">\r\n <span className=\"fg-scan-line\" />\r\n </div>\r\n </>\r\n )}\r\n\r\n {phase === \"done\" && final && (\r\n <div className=\"fg-result\">\r\n <img src={final.url} alt={final.result.accepted ? t.approved : t.rejected} className=\"fg-result-img\" />\r\n {final.result.enhancement && (\r\n <span\r\n className=\"fg-badge\"\r\n data-ai={final.result.warnings?.includes(\"face_restored_ai\") ? \"true\" : undefined}\r\n title={final.result.warnings?.includes(\"face_restored_ai\") ? t.enhancedAi : undefined}\r\n >\r\n {t.enhanced}\r\n {final.result.warnings?.includes(\"face_restored_ai\") && <small>{t.enhancedAi}</small>}\r\n </span>\r\n )}\r\n </div>\r\n )}\r\n\r\n {watermark && phase !== \"off\" && <Emblem className=\"fg-watermark\" />}\r\n\r\n {showIntro && (\r\n <div className=\"fg-intro\" aria-hidden=\"true\">\r\n <Emblem animated className=\"fg-intro-emblem\" />\r\n </div>\r\n )}\r\n\r\n {phase === \"off\" && (\r\n <div className=\"fg-idle\">\r\n <Emblem className=\"fg-idle-emblem\" />\r\n <strong>{t.cameraOff}</strong>\r\n <span>{error ?? t.cameraHint}</span>\r\n </div>\r\n )}\r\n\r\n {(cameraOn || phase === \"processing\") && (\r\n <div className=\"fg-status\" role=\"status\">\r\n <span className={`fg-dot ${phase === \"processing\" ? \"fg-dot-busy\" : ok ? \"fg-dot-ok\" : live ? \"fg-dot-warn\" : \"\"}`} />\r\n <span>{phase === \"processing\" ? (clean ? t.analyzingClean : enhance ? t.analyzingEnhance : t.analyzing) : status}</span>\r\n </div>\r\n )}\r\n {phase === \"done\" && final && (\r\n <div className=\"fg-status\" role=\"status\">\r\n <span className={`fg-dot ${final.result.accepted ? \"fg-dot-ok\" : \"fg-dot-warn\"}`} />\r\n <span>\r\n {final.result.accepted ? t.approved : `${t.rejected}: ${describeFails(final.result.hard_fails ?? final.result.reasons)}`}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"fg-actions\">\r\n {phase === \"done\" || (phase === \"off\" && final) ? (\r\n <button type=\"button\" className=\"fg-button\" onClick={start}>{t.retry}</button>\r\n ) : (\r\n <button type=\"button\" className={`fg-button ${cameraOn ? \"fg-button-outline\" : \"\"}`}\r\n disabled={phase === \"processing\"} onClick={cameraOn ? stop : start}>\r\n {cameraOn ? t.stop : t.start}\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n","/** Caixa do rosto dominante, em frações da imagem (0 a 1). */\r\nexport type FaceBox = { xmin: number; ymin: number; xmax: number; ymax: number };\r\n\r\n/** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */\r\nexport type TokenUsage = {\r\n tokens: number;\r\n /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */\r\n breakdown: { decode: number; validate: number; clean: number; enhance?: number; avatar?: number };\r\n /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */\r\n window?: { used: number; limit: number; hours: number; started_on: string | null; resets_at: string | null };\r\n /** Saldo do teto mensal (renova no aniversário do contrato). */\r\n month?: { used: number; limit: number; cycle_start: string; cycle_end: string };\r\n};\r\n\r\n/**\r\n * Melhoria de imagem (serviço \"validate_enhance\"):\r\n * - \"classic\": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;\r\n * - \"gfpgan\": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).\r\n */\r\nexport type EnhanceMethod = \"classic\" | \"gfpgan\";\r\n\r\n/** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */\r\nexport type ValidationSummary = { accepted: boolean; score: number | null; hard_fails: string[] };\r\n\r\nexport type Enhancement = {\r\n method: EnhanceMethod;\r\n /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */\r\n steps: string[];\r\n /** Dispositivo do GFPGAN (null na clássica). */\r\n device: \"cpu\" | \"gpu\" | null;\r\n before: ValidationSummary;\r\n after: ValidationSummary;\r\n /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */\r\n carried_over: string[];\r\n /** Foto melhorada (JPEG em data URL). */\r\n enhanced_image: string;\r\n};\r\n\r\n/** Formato do avatar (serviço \"avatar\"): redondo (fora do círculo transparente) ou quadrado. */\r\nexport type AvatarShape = \"square\" | \"round\";\r\n\r\n/**\r\n * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em\r\n * cima); na base à esquerda ou à direita.\r\n */\r\nexport type AvatarLayout = \"center\" | \"bottom\" | \"left\" | \"right\";\r\n\r\n/** Dados do avatar gerado (POST /v1/photos/avatar). */\r\nexport type AvatarInfo = {\r\n shape: AvatarShape;\r\n /** Lado em pixels (64 a 1024). */\r\n size: number;\r\n /** Modelo de remoção de fundo que separou a pessoa. */\r\n model: string;\r\n /** \"transparent\", \"image\" (fundo enviado) ou a cor \"#rrggbb\". */\r\n background: string;\r\n /** % do quadro deixado para o fundo (0 com fundo transparente). */\r\n margin?: number;\r\n layout?: AvatarLayout;\r\n};\r\n\r\n/** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */\r\nexport type ValidationWarning = \"face_restored_ai\" | \"gpu_fallback_cpu\" | (string & {});\r\n\r\n/** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */\r\nexport type ValidationResult = {\r\n accepted: boolean;\r\n score: number;\r\n checks: Record<string, boolean | null>;\r\n /** Critérios que reprovaram (ex.: \"eyes_closed\", \"sunglasses\", \"low_light\"). */\r\n hard_fails?: string[];\r\n reasons?: string[];\r\n warnings?: ValidationWarning[];\r\n error?: string;\r\n dominant_face_bbox?: FaceBox | null;\r\n /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */\r\n cleaned_image?: string;\r\n rembg_model?: string;\r\n device?: \"cpu\" | \"gpu\";\r\n timings?: { validation_ms?: number };\r\n usage?: TokenUsage;\r\n /** Presente quando a foto passou pela melhoria (`enhance`). */\r\n enhancement?: Enhancement;\r\n /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */\r\n avatar_image?: string;\r\n avatar?: AvatarInfo;\r\n};\r\n\r\nexport type StreamOptions = {\r\n /** Remover o fundo da foto capturada (serviço \"validate_clean\" contratado). */\r\n clean?: boolean;\r\n model?: \"u2net\" | \"u2netp\" | \"birefnet-portrait\";\r\n device?: \"cpu\" | \"gpu\";\r\n /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */\r\n thresholds?: Record<string, number>;\r\n /**\r\n * Melhorar a foto capturada antes de validar (serviço \"validate_enhance\" contratado). Os quadros ao\r\n * vivo não passam pela melhoria. null/undefined = sem melhoria.\r\n */\r\n enhance?: EnhanceMethod | null;\r\n};\r\n\r\n/** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */\r\nexport type StreamTicket = { ticket: string; url: string; expiresIn?: number };\r\n\r\n/** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */\r\nexport type Stable = { ms: number; needed: number };\r\n\r\nexport type FaceGateErrorCode =\r\n | \"unauthorized\" // 401/4401: chave, token ou ticket inválido/expirado\r\n | \"forbidden\" // 403/4403: serviço não contratado, chave bloqueada, fora do contrato ou IP não liberado\r\n | \"token_month_limit\" // 402/4402: teto mensal de tokens atingido\r\n | \"token_window_limit\" // 429/4429: janela de tokens esgotada (ou muitas requisições)\r\n | \"bad_request\" // 400/4400\r\n | \"camera_denied\"\r\n | \"camera_unavailable\"\r\n | \"network\"\r\n | \"server\";\r\n\r\nexport class FaceGateError extends Error {\r\n constructor(\r\n public code: FaceGateErrorCode,\r\n message: string,\r\n public status?: number,\r\n /** Quando o limite renova (ISO), se a API informou. */\r\n public resetsAt?: string,\r\n ) {\r\n super(message);\r\n this.name = \"FaceGateError\";\r\n }\r\n}\r\n","import { FaceGateError, type FaceGateErrorCode, type Stable, type StreamOptions, type ValidationResult } from \"./types\";\r\n\r\n/**\r\n * Cliente do WebSocket /v1/stream do Face Gate (uma conexão por sessão de câmera).\r\n *\r\n * O navegador manda quadros JPEG (mensagens binárias) num ritmo fixo, o `max_fps` que a API informa no \"ready\":\r\n * ela analisa sempre o mais novo e descarta os que ficaram para trás. Recebe o resultado de cada quadro\r\n * analisado. A API só captura depois de ~0,8 s de quadros aprovados e parados (`stable`), avisa `captured`\r\n * com o seq do melhor quadro e devolve a validação final.\r\n */\r\nexport type StreamHandlers = {\r\n /** `maxFps`: quantos quadros por segundo a API analisa (o ritmo para mandar os quadros). */\r\n onReady?: (maxFps: number) => void;\r\n onFrame?: (seq: number, result: ValidationResult, stable: Stable) => void;\r\n onCaptured?: (seq: number) => void;\r\n onFinal?: (seq: number, result: ValidationResult) => void;\r\n /** Conexão encerrada sem ter sido pedida (`close()`). */\r\n onError?: (error: FaceGateError) => void;\r\n};\r\n\r\nconst CLOSE: Record<number, [FaceGateErrorCode, string]> = {\r\n 4400: [\"bad_request\", \"A API recusou a mensagem da câmera.\"],\r\n 4401: [\"unauthorized\", \"Sessão da câmera expirada. Tente de novo.\"],\r\n 4402: [\"token_month_limit\", \"Limite mensal de tokens atingido.\"],\r\n 4403: [\"forbidden\", \"Acesso negado para esta validação.\"],\r\n 4408: [\"bad_request\", \"A câmera ficou ligada tempo demais.\"],\r\n 4429: [\"token_window_limit\", \"Limite de uso atingido. Aguarde e tente de novo.\"],\r\n 1011: [\"server\", \"Erro interno na API.\"],\r\n};\r\n\r\nconst DEFAULT_FPS = 6;\r\n/** Com mais que isso ainda subindo, o quadro novo é pulado: em rede lenta não forma fila de quadros velhos. */\r\nconst MAX_BUFFERED = 32 * 1024;\r\n\r\nexport type FaceGateStream = {\r\n /**\r\n * Envia o quadro e devolve o seq dele (1, 2, 3… igual ao servidor); null se a conexão não está aberta ou o\r\n * quadro anterior ainda está subindo.\r\n */\r\n sendFrame: (frame: Blob) => number | null;\r\n configure: (options: StreamOptions) => void;\r\n /** Recomeça depois de uma captura, na mesma conexão. */\r\n reset: () => void;\r\n close: () => void;\r\n};\r\n\r\ntype WebSocketLike = new (url: string) => WebSocket;\r\n\r\nexport function openFaceGateStream(\r\n url: string,\r\n ticket: string,\r\n options: StreamOptions,\r\n handlers: StreamHandlers,\r\n WS: WebSocketLike = WebSocket,\r\n): FaceGateStream {\r\n const ws = new WS(url);\r\n ws.binaryType = \"arraybuffer\";\r\n let closedByUs = false;\r\n let finished = false;\r\n let seq = 0;\r\n\r\n ws.onopen = () => ws.send(JSON.stringify({ type: \"start\", token: ticket, ...options }));\r\n ws.onmessage = (e: MessageEvent) => {\r\n if (typeof e.data !== \"string\") return;\r\n const m = JSON.parse(e.data);\r\n if (m.type === \"ready\") handlers.onReady?.(Number(m.max_fps) || DEFAULT_FPS);\r\n else if (m.type === \"frame\") handlers.onFrame?.(m.seq, m.result, m.stable ?? { ms: 0, needed: 0 });\r\n else if (m.type === \"captured\") handlers.onCaptured?.(m.seq);\r\n else if (m.type === \"final\") {\r\n finished = true;\r\n handlers.onFinal?.(m.seq, m.result);\r\n }\r\n };\r\n ws.onclose = (e: CloseEvent) => {\r\n if (closedByUs || finished) return;\r\n const [code, message] = CLOSE[e.code] ?? [\"network\", e.reason || \"Conexão com a API perdida.\"];\r\n handlers.onError?.(new FaceGateError(code, e.reason || message, e.code));\r\n };\r\n\r\n const open = () => ws.readyState === 1;\r\n return {\r\n sendFrame: (frame) => {\r\n if (!open() || ws.bufferedAmount > MAX_BUFFERED) return null;\r\n ws.send(frame);\r\n return ++seq;\r\n },\r\n configure: (o) => {\r\n if (open()) ws.send(JSON.stringify({ type: \"config\", ...o }));\r\n },\r\n reset: () => {\r\n finished = false;\r\n if (open()) ws.send(JSON.stringify({ type: \"reset\" }));\r\n },\r\n close: () => {\r\n closedByUs = true;\r\n if (ws.readyState <= 1) ws.close(1000);\r\n },\r\n };\r\n}\r\n","import { useId } from \"react\";\r\n\r\n/**\r\n * Escudo BelTec (Face Gate, uma solução BelTec): a faixa do escudo Belfort liga três pontos — a lente\r\n * (a caixa que vê), a Central que entende e as pessoas que agem. Com `animated`, se desenha como na\r\n * abertura da câmera do app (CSS em styles.css, classes .fg-emblem-*).\r\n */\r\nexport function Emblem({ animated = false, className }: { animated?: boolean; className?: string }) {\r\n const id = useId().replace(/:/g, \"\");\r\n const shield = \"M4 4.5Q4 4 4.5 4H43.5Q44 4 44 4.5V30C44 42 35 52 24 52C13 52 4 42 4 30Z\";\r\n return (\r\n <svg\r\n viewBox=\"0 0 48 56\"\r\n fill=\"none\"\r\n aria-hidden=\"true\"\r\n className={[className, animated ? \"fg-emblem-animated\" : \"\"].filter(Boolean).join(\" \")}\r\n >\r\n <defs>\r\n <linearGradient id={`${id}g`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\r\n <stop offset=\"0\" stopColor=\"#e3c35c\" />\r\n <stop offset=\"0.55\" stopColor=\"#c9a43e\" />\r\n <stop offset=\"1\" stopColor=\"#a8852a\" />\r\n </linearGradient>\r\n <clipPath id={`${id}c`}>\r\n <path d={shield} />\r\n </clipPath>\r\n </defs>\r\n <path className=\"fg-emblem-shield\" d={shield} fill={`url(#${id}g)`} />\r\n <g clipPath={`url(#${id}c)`}>\r\n <path className=\"fg-emblem-band\" d=\"M-2 41.2L50 10.8\" stroke=\"#24282d\" strokeWidth=\"9.5\" pathLength={1} />\r\n </g>\r\n <path className=\"fg-emblem-line\" d=\"M10.5 33.9L37.5 18.1\" stroke=\"#c9a43e\" strokeWidth=\"0.9\" pathLength={1} />\r\n <circle className=\"fg-emblem-dot\" cx=\"10.5\" cy=\"33.9\" r=\"1.6\" fill=\"#c9a43e\" />\r\n <circle className=\"fg-emblem-dot fg-emblem-dot-2\" cx=\"37.5\" cy=\"18.1\" r=\"1.6\" fill=\"#c9a43e\" />\r\n <g className=\"fg-emblem-lens\">\r\n <circle cx=\"24\" cy=\"26\" r=\"7.4\" fill=\"#24282d\" />\r\n <circle cx=\"24\" cy=\"26\" r=\"5.3\" stroke=\"#c9a43e\" strokeWidth=\"1.6\" />\r\n <circle cx=\"24\" cy=\"26\" r=\"2.6\" fill=\"#15181b\" stroke=\"#c9a43e\" strokeWidth=\"0.8\" />\r\n <circle cx=\"23\" cy=\"25\" r=\"0.8\" fill=\"#f1dc93\" />\r\n </g>\r\n </svg>\r\n );\r\n}\r\n","// Enquadramento automático da webcam (como o \"Enquadramento Centralizado\" do iPad): um recorte da imagem da\r\n// câmera, na mesma proporção, que acompanha o rosto e o mantém no centro. A prévia mostra o recorte e é ele que\r\n// vai para a validação, então a foto já sai centralizada. Funções puras (testáveis) + a medição da área útil.\r\n\r\nimport type { FaceBox } from \"../core/types\";\r\n\r\n/** Recorte: canto (`x`, `y`) e lado (`s`) em fração da imagem; `s` = 1 é a imagem inteira. */\r\nexport type Crop = { x: number; y: number; s: number };\r\n/** Área útil da imagem (fração do quadro): sem as tarjas pretas que algumas câmeras/drivers acrescentam. */\r\nexport type Area = { x: number; y: number; w: number; h: number };\r\n\r\nexport const FULL: Crop = { x: 0, y: 0, s: 1 };\r\nexport const WHOLE: Area = { x: 0, y: 0, w: 1, h: 1 };\r\n/** O rosto mira ~40% da altura do quadro (o analyzer pede área mínima de 6%). */\r\nexport const FACE_H_TARGET = 0.4;\r\n/** Zoom de até 2x (a câmera abre em 1280×960 para o recorte não perder nitidez). */\r\nexport const MIN_CROP = 0.5;\r\n/** Só muda o alvo se ele andar mais que isso: parado, sem tremor (a janela estável da captura conta). */\r\nexport const RETARGET = 0.03;\r\n/** Na webcam, captura só com o rosto realmente no centro: tolerância de 8% (o padrão do analyzer é 22%). */\r\nexport const CENTER_TOLERANCE = 0.08;\r\n\r\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\r\n\r\n/** Maior recorte (proporção do quadro) dentro da área útil, centralizado nela: o quadro \"sem rosto\". */\r\nexport function baseCrop(a: Area): Crop {\r\n const s = Math.min(a.w, a.h);\r\n return { s, x: a.x + (a.w - s) / 2, y: a.y + (a.h - s) / 2 };\r\n}\r\n\r\n/** Prende o recorte dentro da área útil (tamanho e posição). */\r\nexport function fitCrop(c: Crop, a: Area): Crop {\r\n const s = Math.min(c.s, a.w, a.h);\r\n return { s, x: clamp(c.x, a.x, a.x + a.w - s), y: clamp(c.y, a.y, a.y + a.h - s) };\r\n}\r\n\r\n/**\r\n * Alvo do recorte para o rosto `box`, em coordenadas do recorte `used` (o do quadro analisado): centraliza o\r\n * rosto e o deixa com ~40% da altura, sem sair da área útil.\r\n */\r\nexport function aimCrop(box: FaceBox, used: Crop, a: Area): Crop {\r\n const cx = used.x + ((box.xmin + box.xmax) / 2) * used.s;\r\n const cy = used.y + ((box.ymin + box.ymax) / 2) * used.s;\r\n const maxS = Math.min(a.w, a.h);\r\n const s = clamp(((box.ymax - box.ymin) * used.s) / FACE_H_TARGET, Math.min(MIN_CROP, maxS), maxS);\r\n return fitCrop({ s, x: cx - s / 2, y: cy - s / 2 }, a);\r\n}\r\n\r\n/** Andou o bastante para valer trocar o alvo? */\r\nexport const moved = (a: Crop, b: Crop) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.s - b.s) > RETARGET;\r\n\r\n/** Um passo do movimento suave do recorte atual `c` até o alvo `t`. */\r\nexport const ease = (c: Crop, t: Crop, k = 0.12): Crop => ({\r\n x: c.x + (t.x - c.x) * k,\r\n y: c.y + (t.y - c.y) * k,\r\n s: c.s + (t.s - c.s) * k,\r\n});\r\n\r\n/**\r\n * CSS da prévia do recorte, para uma camada com `transform-origin: 0 0` (no estilo, não em classe) e o vídeo\r\n * ocupando-a inteira. `mirror`: como um espelho (a pessoa se orienta melhor); o quadro enviado não é espelhado.\r\n */\r\nexport function cropTransform(c: Crop, mirror = true): string {\r\n const k = 1 / c.s;\r\n const move = `translate(${-c.x * 100}%, ${-c.y * 100}%)`;\r\n return mirror ? `translateX(100%) scale(${-k}, ${k}) ${move}` : `scale(${k}) ${move}`;\r\n}\r\n\r\n/**\r\n * Leva o que foi desenhado no recorte do quadro analisado (`used`) para o recorte que está na tela agora (`now`):\r\n * o resultado chega depois que o enquadramento andou, e sem isso a moldura fica fora do rosto. Para uma camada do\r\n * tamanho da prévia, com `transform-origin: 0 0`, cujo conteúdo usa `boxStyle(box, mirror)`.\r\n */\r\nexport function overlayTransform(used: Crop, now: Crop, mirror = true): string {\r\n const k = used.s / now.s;\r\n const dx = (used.x - now.x) / now.s;\r\n const tx = mirror ? 1 - k - dx : dx;\r\n const ty = (used.y - now.y) / now.s;\r\n return `translate(${tx * 100}%, ${ty * 100}%) scale(${k})`;\r\n}\r\n\r\n/** Caixa do rosto na prévia (espelhada, se for o caso), em % da camada. */\r\nexport function boxStyle(b: FaceBox, mirror: boolean) {\r\n const left = mirror ? 1 - b.xmax : b.xmin;\r\n return {\r\n left: `${left * 100}%`,\r\n top: `${b.ymin * 100}%`,\r\n width: `${(b.xmax - b.xmin) * 100}%`,\r\n height: `${(b.ymax - b.ymin) * 100}%`,\r\n };\r\n}\r\n\r\n/**\r\n * Área útil numa miniatura RGBA (`px`, `w` × `h`): colunas/linhas das bordas sem nenhum ponto claro são tarja.\r\n * Ambiente escuro (menos de 40% útil) devolve a imagem inteira.\r\n */\r\nexport function areaFromPixels(px: Uint8ClampedArray, w: number, h: number): Area {\r\n const lit = (x: number, y: number) => {\r\n const i = (y * w + x) * 4;\r\n return Math.max(px[i] ?? 0, px[i + 1] ?? 0, px[i + 2] ?? 0) > 24;\r\n };\r\n const colLit = (x: number) => Array.from({ length: h }, (_, y) => lit(x, y)).some(Boolean);\r\n const rowLit = (y: number) => Array.from({ length: w }, (_, x) => lit(x, y)).some(Boolean);\r\n let x0 = 0;\r\n let x1 = w - 1;\r\n let y0 = 0;\r\n let y1 = h - 1;\r\n while (x0 < x1 && !colLit(x0)) x0++;\r\n while (x1 > x0 && !colLit(x1)) x1--;\r\n while (y0 < y1 && !rowLit(y0)) y0++;\r\n while (y1 > y0 && !rowLit(y1)) y1--;\r\n if (x0 === 0 && x1 === w - 1 && y0 === 0 && y1 === h - 1) return WHOLE;\r\n // 1 px de margem da miniatura: a borda da tarja costuma ter um degradê\r\n const area = { x: (x0 + 1) / w, y: (y0 + 1) / h, w: Math.max((x1 - x0 - 1) / w, 0), h: Math.max((y1 - y0 - 1) / h, 0) };\r\n return area.w < 0.4 || area.h < 0.4 ? WHOLE : area;\r\n}\r\n\r\n/** Imagem quase toda preta (média < 8 de 255)? Câmera que não entrega imagem na resolução pedida. */\r\nexport function isDark(px: Uint8ClampedArray): boolean {\r\n let sum = 0;\r\n let n = 0;\r\n for (let i = 0; i < px.length; i += 4 * 97) {\r\n sum += (px[i] ?? 0) + (px[i + 1] ?? 0) + (px[i + 2] ?? 0);\r\n n += 3;\r\n }\r\n return n > 0 && sum / n < 8;\r\n}\r\n\r\n/** Limiares da webcam: centro com tolerância de 8% (ou a que o integrador pediu, se for mais rígida). */\r\nexport function framingThresholds(th: Record<string, number> | undefined): Record<string, number> {\r\n const t = th ?? {};\r\n return {\r\n ...t,\r\n CENTER_TOL_X: Math.min(t.CENTER_TOL_X ?? CENTER_TOLERANCE, CENTER_TOLERANCE),\r\n CENTER_TOL_Y: Math.min(t.CENTER_TOL_Y ?? CENTER_TOLERANCE, CENTER_TOLERANCE),\r\n };\r\n}\r\n","/** Textos do componente (pt-BR). Passe `labels` no <FaceGate> para trocar qualquer um. */\r\nexport const DEFAULT_LABELS = {\r\n start: \"Ligar câmera\",\r\n stop: \"Desligar câmera\",\r\n retry: \"Nova captura\",\r\n connecting: \"Conectando…\",\r\n searching: \"Procurando rosto…\",\r\n hold: \"Fique parado…\",\r\n allGood: \"Tudo certo, segure a posição…\",\r\n fix: \"Corrigir\",\r\n analyzing: \"Analisando o rosto…\",\r\n analyzingClean: \"Analisando o rosto e removendo o fundo…\",\r\n analyzingEnhance: \"Melhorando a imagem e analisando o rosto…\",\r\n enhanced: \"Imagem melhorada\",\r\n enhancedAi: \"Rosto restaurado com IA: confira se a foto continua fiel à pessoa.\",\r\n approved: \"Foto aprovada\",\r\n rejected: \"Foto reprovada\",\r\n cameraOff: \"Câmera desligada\",\r\n cameraHint: \"A foto é capturada sozinha quando todos os critérios passam.\",\r\n insecure: \"A câmera exige HTTPS (ou localhost).\",\r\n noCamera: \"Este navegador não permite acesso à câmera.\",\r\n denied: \"Permissão de câmera negada. Libere o acesso na barra do navegador.\",\r\n notFound: \"Nenhuma câmera encontrada.\",\r\n busy: \"A câmera está em uso por outro aplicativo. Feche-o e tente de novo.\",\r\n};\r\n\r\nexport type FaceGateLabels = typeof DEFAULT_LABELS;\r\n\r\n/** Critério reprovado (hard_fails da API) → orientação curta para quem está na câmera. */\r\nexport const FAIL_LABELS: Record<string, string> = {\r\n no_face: \"enquadre o rosto\",\r\n multiple_faces: \"só uma pessoa na imagem\",\r\n face_too_small: \"aproxime-se da câmera\",\r\n face_off_center: \"centralize o rosto\",\r\n eyes_closed: \"abra os olhos\",\r\n not_looking_at_camera: \"olhe para a câmera\",\r\n head_too_tilted: \"endireite a cabeça\",\r\n image_blurry: \"fique parado (imagem tremida)\",\r\n poor_lighting: \"melhore a luz no rosto\",\r\n overexposed: \"luz forte demais no rosto\",\r\n glasses_sunglasses: \"retire os óculos escuros\",\r\n helmet_detected: \"retire o capacete\",\r\n covered_face: \"descubra o rosto\",\r\n smile_detected: \"expressão neutra\",\r\n};\r\n\r\nexport function describeFails(codes: string[] = []): string {\r\n return codes.map((c) => FAIL_LABELS[c] ?? c).join(\", \");\r\n}\r\n"],"mappings":"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","names":["import_react","import_jsx_runtime","box","k"]}
@@ -0,0 +1,259 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties } from 'react';
3
+
4
+ /** Caixa do rosto dominante, em frações da imagem (0 a 1). */
5
+ type FaceBox = {
6
+ xmin: number;
7
+ ymin: number;
8
+ xmax: number;
9
+ ymax: number;
10
+ };
11
+ /** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */
12
+ type TokenUsage = {
13
+ tokens: number;
14
+ /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */
15
+ breakdown: {
16
+ decode: number;
17
+ validate: number;
18
+ clean: number;
19
+ enhance?: number;
20
+ avatar?: number;
21
+ };
22
+ /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */
23
+ window?: {
24
+ used: number;
25
+ limit: number;
26
+ hours: number;
27
+ started_on: string | null;
28
+ resets_at: string | null;
29
+ };
30
+ /** Saldo do teto mensal (renova no aniversário do contrato). */
31
+ month?: {
32
+ used: number;
33
+ limit: number;
34
+ cycle_start: string;
35
+ cycle_end: string;
36
+ };
37
+ };
38
+ /**
39
+ * Melhoria de imagem (serviço "validate_enhance"):
40
+ * - "classic": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;
41
+ * - "gfpgan": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).
42
+ */
43
+ type EnhanceMethod = "classic" | "gfpgan";
44
+ /** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */
45
+ type ValidationSummary = {
46
+ accepted: boolean;
47
+ score: number | null;
48
+ hard_fails: string[];
49
+ };
50
+ type Enhancement = {
51
+ method: EnhanceMethod;
52
+ /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */
53
+ steps: string[];
54
+ /** Dispositivo do GFPGAN (null na clássica). */
55
+ device: "cpu" | "gpu" | null;
56
+ before: ValidationSummary;
57
+ after: ValidationSummary;
58
+ /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */
59
+ carried_over: string[];
60
+ /** Foto melhorada (JPEG em data URL). */
61
+ enhanced_image: string;
62
+ };
63
+ /** Formato do avatar (serviço "avatar"): redondo (fora do círculo transparente) ou quadrado. */
64
+ type AvatarShape = "square" | "round";
65
+ /**
66
+ * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em
67
+ * cima); na base à esquerda ou à direita.
68
+ */
69
+ type AvatarLayout = "center" | "bottom" | "left" | "right";
70
+ /** Dados do avatar gerado (POST /v1/photos/avatar). */
71
+ type AvatarInfo = {
72
+ shape: AvatarShape;
73
+ /** Lado em pixels (64 a 1024). */
74
+ size: number;
75
+ /** Modelo de remoção de fundo que separou a pessoa. */
76
+ model: string;
77
+ /** "transparent", "image" (fundo enviado) ou a cor "#rrggbb". */
78
+ background: string;
79
+ /** % do quadro deixado para o fundo (0 com fundo transparente). */
80
+ margin?: number;
81
+ layout?: AvatarLayout;
82
+ };
83
+ /** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */
84
+ type ValidationWarning = "face_restored_ai" | "gpu_fallback_cpu" | (string & {});
85
+ /** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */
86
+ type ValidationResult = {
87
+ accepted: boolean;
88
+ score: number;
89
+ checks: Record<string, boolean | null>;
90
+ /** Critérios que reprovaram (ex.: "eyes_closed", "sunglasses", "low_light"). */
91
+ hard_fails?: string[];
92
+ reasons?: string[];
93
+ warnings?: ValidationWarning[];
94
+ error?: string;
95
+ dominant_face_bbox?: FaceBox | null;
96
+ /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */
97
+ cleaned_image?: string;
98
+ rembg_model?: string;
99
+ device?: "cpu" | "gpu";
100
+ timings?: {
101
+ validation_ms?: number;
102
+ };
103
+ usage?: TokenUsage;
104
+ /** Presente quando a foto passou pela melhoria (`enhance`). */
105
+ enhancement?: Enhancement;
106
+ /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */
107
+ avatar_image?: string;
108
+ avatar?: AvatarInfo;
109
+ };
110
+ type StreamOptions = {
111
+ /** Remover o fundo da foto capturada (serviço "validate_clean" contratado). */
112
+ clean?: boolean;
113
+ model?: "u2net" | "u2netp" | "birefnet-portrait";
114
+ device?: "cpu" | "gpu";
115
+ /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */
116
+ thresholds?: Record<string, number>;
117
+ /**
118
+ * Melhorar a foto capturada antes de validar (serviço "validate_enhance" contratado). Os quadros ao
119
+ * vivo não passam pela melhoria. null/undefined = sem melhoria.
120
+ */
121
+ enhance?: EnhanceMethod | null;
122
+ };
123
+ /** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */
124
+ type StreamTicket = {
125
+ ticket: string;
126
+ url: string;
127
+ expiresIn?: number;
128
+ };
129
+ /** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */
130
+ type Stable = {
131
+ ms: number;
132
+ needed: number;
133
+ };
134
+ type FaceGateErrorCode = "unauthorized" | "forbidden" | "token_month_limit" | "token_window_limit" | "bad_request" | "camera_denied" | "camera_unavailable" | "network" | "server";
135
+ declare class FaceGateError extends Error {
136
+ code: FaceGateErrorCode;
137
+ status?: number | undefined;
138
+ /** Quando o limite renova (ISO), se a API informou. */
139
+ resetsAt?: string | undefined;
140
+ constructor(code: FaceGateErrorCode, message: string, status?: number | undefined,
141
+ /** Quando o limite renova (ISO), se a API informou. */
142
+ resetsAt?: string | undefined);
143
+ }
144
+
145
+ /** Textos do componente (pt-BR). Passe `labels` no <FaceGate> para trocar qualquer um. */
146
+ declare const DEFAULT_LABELS: {
147
+ start: string;
148
+ stop: string;
149
+ retry: string;
150
+ connecting: string;
151
+ searching: string;
152
+ hold: string;
153
+ allGood: string;
154
+ fix: string;
155
+ analyzing: string;
156
+ analyzingClean: string;
157
+ analyzingEnhance: string;
158
+ enhanced: string;
159
+ enhancedAi: string;
160
+ approved: string;
161
+ rejected: string;
162
+ cameraOff: string;
163
+ cameraHint: string;
164
+ insecure: string;
165
+ noCamera: string;
166
+ denied: string;
167
+ notFound: string;
168
+ busy: string;
169
+ };
170
+ type FaceGateLabels = typeof DEFAULT_LABELS;
171
+ /** Critério reprovado (hard_fails da API) → orientação curta para quem está na câmera. */
172
+ declare const FAIL_LABELS: Record<string, string>;
173
+ declare function describeFails(codes?: string[]): string;
174
+
175
+ type FaceGateCapture = {
176
+ /**
177
+ * Foto final: sem fundo (PNG) quando `clean`; senão a melhorada (JPEG) quando `enhance`; senão o quadro
178
+ * capturado (JPEG). Data URL ou blob URL.
179
+ */
180
+ image: string;
181
+ /** O quadro capturado, como enviado à API. */
182
+ frame: Blob;
183
+ result: ValidationResult;
184
+ };
185
+ type FaceGateProps = StreamOptions & {
186
+ /** Pede ao SEU servidor um ticket de uso único (ex.: fetch("/api/face-gate/ticket", { method: "POST" })). */
187
+ getTicket: () => Promise<StreamTicket>;
188
+ /** Foto capturada e validada (aprovada ou reprovada na validação final). */
189
+ onCapture?: (capture: FaceGateCapture) => void;
190
+ /** Cada quadro analisado ao vivo. */
191
+ onLive?: (result: ValidationResult) => void;
192
+ onError?: (error: FaceGateError) => void;
193
+ /** Liga a câmera ao montar (padrão: espera o clique em "Ligar câmera"). */
194
+ autoStart?: boolean;
195
+ /** Abertura animada do escudo ao ligar a câmera (padrão: true; desligada com prefers-reduced-motion). */
196
+ intro?: boolean;
197
+ /** Marca d'água do escudo no canto do vídeo (padrão: true). */
198
+ watermark?: boolean;
199
+ /**
200
+ * Enquadramento automático (padrão: true): a imagem aproxima e acompanha o rosto, mantendo-o no centro; a foto
201
+ * capturada é esse recorte e só sai com o rosto centralizado (tolerância de 8%). Ignora as tarjas pretas que
202
+ * algumas câmeras acrescentam.
203
+ */
204
+ autoFrame?: boolean;
205
+ /** Prévia espelhada, como um espelho (padrão: true). A foto enviada e a final não são espelhadas. */
206
+ mirror?: boolean;
207
+ labels?: Partial<FaceGateLabels>;
208
+ className?: string;
209
+ style?: CSSProperties;
210
+ };
211
+ /**
212
+ * Webcam com validação em tempo real do Face Gate: uma conexão WebSocket por sessão, mira que segue o
213
+ * rosto, captura automática só depois de ~0,8 s aprovado e parado, scanner sobre a foto até a resposta.
214
+ * Importe também "@beltec/face-gate/styles.css".
215
+ */
216
+ declare function FaceGate({ getTicket, onCapture, onLive, onError, autoStart, intro, watermark, labels, className, style, clean, model, device, thresholds, enhance, autoFrame, mirror, }: FaceGateProps): react.JSX.Element;
217
+
218
+ /**
219
+ * Escudo BelTec (Face Gate, uma solução BelTec): a faixa do escudo Belfort liga três pontos — a lente
220
+ * (a caixa que vê), a Central que entende e as pessoas que agem. Com `animated`, se desenha como na
221
+ * abertura da câmera do app (CSS em styles.css, classes .fg-emblem-*).
222
+ */
223
+ declare function Emblem({ animated, className }: {
224
+ animated?: boolean;
225
+ className?: string;
226
+ }): react.JSX.Element;
227
+
228
+ /**
229
+ * Cliente do WebSocket /v1/stream do Face Gate (uma conexão por sessão de câmera).
230
+ *
231
+ * O navegador manda quadros JPEG (mensagens binárias) num ritmo fixo, o `max_fps` que a API informa no "ready":
232
+ * ela analisa sempre o mais novo e descarta os que ficaram para trás. Recebe o resultado de cada quadro
233
+ * analisado. A API só captura depois de ~0,8 s de quadros aprovados e parados (`stable`), avisa `captured`
234
+ * com o seq do melhor quadro e devolve a validação final.
235
+ */
236
+ type StreamHandlers = {
237
+ /** `maxFps`: quantos quadros por segundo a API analisa (o ritmo para mandar os quadros). */
238
+ onReady?: (maxFps: number) => void;
239
+ onFrame?: (seq: number, result: ValidationResult, stable: Stable) => void;
240
+ onCaptured?: (seq: number) => void;
241
+ onFinal?: (seq: number, result: ValidationResult) => void;
242
+ /** Conexão encerrada sem ter sido pedida (`close()`). */
243
+ onError?: (error: FaceGateError) => void;
244
+ };
245
+ type FaceGateStream = {
246
+ /**
247
+ * Envia o quadro e devolve o seq dele (1, 2, 3… igual ao servidor); null se a conexão não está aberta ou o
248
+ * quadro anterior ainda está subindo.
249
+ */
250
+ sendFrame: (frame: Blob) => number | null;
251
+ configure: (options: StreamOptions) => void;
252
+ /** Recomeça depois de uma captura, na mesma conexão. */
253
+ reset: () => void;
254
+ close: () => void;
255
+ };
256
+ type WebSocketLike = new (url: string) => WebSocket;
257
+ declare function openFaceGateStream(url: string, ticket: string, options: StreamOptions, handlers: StreamHandlers, WS?: WebSocketLike): FaceGateStream;
258
+
259
+ export { type AvatarInfo, type AvatarLayout, type AvatarShape, DEFAULT_LABELS, Emblem, type EnhanceMethod, type Enhancement, FAIL_LABELS, type FaceBox, FaceGate, type FaceGateCapture, FaceGateError, type FaceGateErrorCode, type FaceGateLabels, type FaceGateProps, type FaceGateStream, type Stable, type StreamHandlers, type StreamOptions, type StreamTicket, type TokenUsage, type ValidationResult, type ValidationSummary, type ValidationWarning, describeFails, openFaceGateStream };