@beltec/face-gate 0.1.0 → 0.1.2
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/README.md +218 -218
- package/dist/index.cjs +46 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +46 -9
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
# @beltec/face-gate
|
|
2
|
-
|
|
3
|
-
**Face Gate, uma solução BelTec** — validação de fotos de rosto para o cadastro de visitantes e moradores no
|
|
4
|
-
controle de acesso. Este pacote traz:
|
|
5
|
-
|
|
6
|
-
- **`<FaceGate>`** (React): webcam com validação em **tempo real** — mira que segue o rosto, orientação do que
|
|
7
|
-
corrigir, captura automática só quando a pessoa fica parada com tudo aprovado, scanner sobre a foto e a foto
|
|
8
|
-
final (com o fundo removido, se contratado).
|
|
9
|
-
- **`@beltec/face-gate/server`** (Node/Next): guarda a sua chave, emite o ticket do navegador e valida arquivos.
|
|
10
|
-
|
|
11
|
-
O `client_secret` **nunca** vai ao navegador: o seu servidor pede um ticket de uso único (60 s) e só ele chega à
|
|
12
|
-
página.
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
navegador ──POST /api/face-gate/ticket──▶ seu servidor ──chave──▶ Face Gate API
|
|
16
|
-
navegador ◀──────── ticket (60 s) ────────┘
|
|
17
|
-
navegador ══ WebSocket (ticket) ══▶ Face Gate API (quadros ao vivo → captura → resultado)
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Instalação
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @beltec/face-gate
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
React 18+ no navegador; Node 18.17+ no servidor. Crie a chave no portal do Face Gate
|
|
27
|
-
(**Chaves de acesso**): você recebe o `client_id` e o `client_secret` (este aparece uma vez só).
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# .env do seu servidor
|
|
31
|
-
FACE_GATE_CLIENT_ID=...
|
|
32
|
-
FACE_GATE_CLIENT_SECRET=...
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Next.js (App Router)
|
|
36
|
-
|
|
37
|
-
**1. Rota que emite o ticket** — `app/api/face-gate/ticket/route.ts`
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { createFaceGate } from "@beltec/face-gate/server";
|
|
41
|
-
|
|
42
|
-
const faceGate = createFaceGate({
|
|
43
|
-
clientId: process.env.FACE_GATE_CLIENT_ID!,
|
|
44
|
-
clientSecret: process.env.FACE_GATE_CLIENT_SECRET!,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Só usuários logados no SEU sistema abrem a câmera (cada captura consome tokens do seu plano).
|
|
48
|
-
export const POST = faceGate.ticketHandler({
|
|
49
|
-
authorize: async (req) => Boolean(await getSessionFromYourApp(req)),
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**2. A tela** — um Client Component
|
|
54
|
-
|
|
55
|
-
```tsx
|
|
56
|
-
"use client";
|
|
57
|
-
import { FaceGate } from "@beltec/face-gate";
|
|
58
|
-
import "@beltec/face-gate/styles.css";
|
|
59
|
-
|
|
60
|
-
export function CadastroFoto() {
|
|
61
|
-
return (
|
|
62
|
-
<FaceGate
|
|
63
|
-
clean // devolve a foto sem fundo (serviço "validar e remover fundo" contratado)
|
|
64
|
-
getTicket={async () => {
|
|
65
|
-
const r = await fetch("/api/face-gate/ticket", { method: "POST" });
|
|
66
|
-
if (!r.ok) throw new Error("Não foi possível abrir a câmera");
|
|
67
|
-
return r.json();
|
|
68
|
-
}}
|
|
69
|
-
onCapture={({ image, result }) => {
|
|
70
|
-
if (result.accepted) salvarFoto(image); // data URL (PNG sem fundo ou JPEG do quadro)
|
|
71
|
-
}}
|
|
72
|
-
onError={(e) => console.warn(e.code, e.message)}
|
|
73
|
-
/>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Validar um arquivo (sem webcam)
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
import { createFaceGate } from "@beltec/face-gate/server";
|
|
82
|
-
|
|
83
|
-
const faceGate = createFaceGate({ clientId, clientSecret });
|
|
84
|
-
const result = await faceGate.validate(fileBuffer, { clean: true, model: "birefnet-portrait" });
|
|
85
|
-
|
|
86
|
-
result.accepted; // true | false
|
|
87
|
-
result.hard_fails; // ex.: ["eyes_closed", "glasses_sunglasses"]
|
|
88
|
-
result.cleaned_image; // PNG sem fundo (data URL), quando clean
|
|
89
|
-
result.usage?.tokens; // tokens gastos nesta análise
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Com melhoria de imagem: `faceGate.validate(fileBuffer, { enhance: "classic" })` (veja abaixo).
|
|
93
|
-
|
|
94
|
-
Express/Fastify funcionam igual: use `createStreamTicket()` na sua rota e devolva `{ ticket, url }` ao navegador.
|
|
95
|
-
|
|
96
|
-
## `<FaceGate>` — propriedades
|
|
97
|
-
|
|
98
|
-
| Prop | Tipo | Padrão | |
|
|
99
|
-
|---|---|---|---|
|
|
100
|
-
| `getTicket` | `() => Promise<{ ticket, url }>` | — | chama a sua rota de ticket |
|
|
101
|
-
| `onCapture` | `({ image, frame, result }) => void` | — | foto capturada e validada |
|
|
102
|
-
| `onLive` | `(result) => void` | — | cada quadro analisado ao vivo |
|
|
103
|
-
| `onError` | `(error: FaceGateError) => void` | — | câmera negada, limite de tokens, rede… |
|
|
104
|
-
| `clean` | `boolean` | `false` | remover o fundo da foto capturada |
|
|
105
|
-
| `model` | `"u2net" \| "u2netp" \| "birefnet-portrait"` | servidor | modelo da remoção de fundo |
|
|
106
|
-
| `device` | `"cpu" \| "gpu"` | servidor | onde rodar a remoção de fundo |
|
|
107
|
-
| `enhance` | `"classic" \| "gfpgan" \| null` | `null` | melhorar a foto capturada antes de validar (serviço "validar com melhoria") |
|
|
108
|
-
| `thresholds` | `Record<string, number>` | servidor | limiares (nomes de `GET /v1/thresholds`) |
|
|
109
|
-
| `autoFrame` | `boolean` | `true` | enquadramento automático: a imagem aproxima e acompanha o rosto, mantendo-o no centro; a foto capturada é esse recorte e só sai com o rosto centralizado (tolerância de 8%, ou a sua em `thresholds` se for mais rígida). Ignora as tarjas pretas que algumas câmeras acrescentam |
|
|
110
|
-
| `mirror` | `boolean` | `true` | prévia espelhada, como um espelho (a foto enviada e a final não são espelhadas) |
|
|
111
|
-
| `autoStart` | `boolean` | `false` | ligar a câmera ao montar |
|
|
112
|
-
| `intro` | `boolean` | `true` | abertura animada do escudo |
|
|
113
|
-
| `watermark` | `boolean` | `true` | escudo no canto do vídeo |
|
|
114
|
-
| `labels` | `Partial<FaceGateLabels>` | pt-BR | troque qualquer texto |
|
|
115
|
-
|
|
116
|
-
### Aparência
|
|
117
|
-
|
|
118
|
-
Os estilos usam o prefixo `.fg-` e variáveis CSS:
|
|
119
|
-
|
|
120
|
-
```css
|
|
121
|
-
.minha-tela .fg-root {
|
|
122
|
-
--fg-accent: #c9a43e; /* mira, botão, barra de "fique parado" */
|
|
123
|
-
--fg-accent-text: #1d2125; /* texto sobre o botão */
|
|
124
|
-
--fg-surface: #1d2125; /* fundo do vídeo */
|
|
125
|
-
--fg-radius: 12px;
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Respeita `prefers-reduced-motion` (sem animações).
|
|
130
|
-
|
|
131
|
-
## Melhoria de imagem
|
|
132
|
-
|
|
133
|
-
Serviço contratável **"Validar com melhoria"** (scope `validate_enhance`). A API melhora a foto, valida a
|
|
134
|
-
versão melhorada e devolve `enhancement` com o resultado **antes** (original) e **depois** (melhorada) e a
|
|
135
|
-
imagem melhorada (`enhancement.enhanced_image`, JPEG). No `<FaceGate enhance="classic">`, só a foto
|
|
136
|
-
capturada passa pela melhoria (os quadros ao vivo não), o `onCapture` entrega a melhorada em `image` e a
|
|
137
|
-
tela mostra o selo "Imagem melhorada".
|
|
138
|
-
|
|
139
|
-
| Método | O que faz | Custo |
|
|
140
|
-
|---|---|---|
|
|
141
|
-
| `classic` | balanço de branco, exposição, contraste local, redução de ruído e nitidez leve. Determinística, não altera o rosto | baixo |
|
|
142
|
-
| `gfpgan` | a clássica + restauração do rosto com IA (GFPGAN). Recria detalhes do rosto: a resposta traz o aviso `face_restored_ai` e o componente mostra o alerta | alto |
|
|
143
|
-
|
|
144
|
-
**Regra:** a melhoria só corrige luz (`poor_lighting`, `overexposed`) e rosto não encontrado por falta de
|
|
145
|
-
luz (`no_face`). As demais falhas da foto original — desfoque, olhos fechados, óculos escuros, pose… —
|
|
146
|
-
continuam reprovando (`enhancement.carried_over`). Com `clean`, o fundo é removido da foto melhorada (exige
|
|
147
|
-
também o serviço "validar e remover fundo").
|
|
148
|
-
|
|
149
|
-
**Tokens:** a validação roda duas vezes (antes e depois) e soma a etapa `usage.breakdown.enhance`
|
|
150
|
-
(a clássica é barata; o GFPGAN custa bem mais).
|
|
151
|
-
|
|
152
|
-
## Avatar
|
|
153
|
-
|
|
154
|
-
Serviço **Avatar** contratado (`POST /v1/photos/avatar`): valida a foto e, se aprovada, devolve o avatar só com a
|
|
155
|
-
pessoa do rosto em foco — o fundo e outros elementos ou pessoas são removidos —, recortado centrado no rosto.
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import { createFaceGate } from "@beltec/face-gate/server";
|
|
159
|
-
|
|
160
|
-
const faceGate = createFaceGate({ clientId, clientSecret });
|
|
161
|
-
const r = await faceGate.avatar(fileBuffer, {
|
|
162
|
-
shape: "round", // "round" (padrão; fora do círculo transparente) | "square"
|
|
163
|
-
size: 512, // 64 a 1024
|
|
164
|
-
backgroundColor: "#1d252a", // ou background: <imagem> (tem prioridade); sem nenhum = PNG transparente
|
|
165
|
-
margin: 25, // só com fundo: % do quadro deixado para o fundo (0 a 60)
|
|
166
|
-
layout: "bottom", // só com fundo: "center" | "bottom" | "left" | "right"
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (r.accepted) r.avatar_image; // data:image/png;base64,...
|
|
170
|
-
else r.hard_fails; // sem avatar: os mesmos motivos da validação
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
| Opção | Tipo | Padrão | |
|
|
174
|
-
|---|---|---|---|
|
|
175
|
-
| `shape` | `"round" \| "square"` | `"round"` | formato do recorte |
|
|
176
|
-
| `size` | `number` | `512` | lado em pixels (64 a 1024) |
|
|
177
|
-
| `backgroundColor` | `string` | — | cor sólida `#RRGGBB` |
|
|
178
|
-
| `background` | `Blob \| ArrayBuffer \| Uint8Array` | — | imagem de fundo (preenche o quadro) |
|
|
179
|
-
| `template` | `number` | — | id de um **template de avatar** salvo no portal (Avatares): formato, tamanho, fundo e enquadramento prontos, sem reenviar a imagem; as outras opções têm prioridade |
|
|
180
|
-
| `backgroundPosition` | `{ x?, y?, zoom? }` | centro, `zoom` 100 | **só com `background`**: qual parte da imagem aparece — `x`/`y` de 0 a 100 (50 = centro, como o `object-position` do CSS) e `zoom` de 100 a 300 % |
|
|
181
|
-
| `margin` | `number` | `0` | **só com fundo**: % do quadro deixado para o fundo em volta da pessoa (0 a 60) |
|
|
182
|
-
| `layout` | `"center" \| "bottom" \| "left" \| "right"` | `"center"` | **só com fundo**: onde a pessoa fica (centro, apoiada na base, base à esquerda/direita) |
|
|
183
|
-
| `model` / `device` | | servidor (BiRefNet) | modelo de remoção de fundo que separa a pessoa |
|
|
184
|
-
| `thresholds` | `Record<string, number>` | servidor | limiares da validação |
|
|
185
|
-
|
|
186
|
-
O resultado traz também `avatar` (`shape`, `size`, `model`, `background`, `margin`, `layout`). **Tokens:** soma a etapa `avatar`
|
|
187
|
-
em `usage.breakdown` (a separação da pessoa roda na imagem analisada inteira).
|
|
188
|
-
|
|
189
|
-
## Tokens e erros
|
|
190
|
-
|
|
191
|
-
Cada foto capturada (e cada `validate`) consome **tokens** do seu plano — megapixels processados × peso de cada
|
|
192
|
-
etapa; foto maior ou remoção de fundo com BiRefNet gastam mais. Os quadros ao vivo da webcam não consomem.
|
|
193
|
-
O plano limita os tokens por **janela** (ex.: 5 h a partir do 1º uso) e por **mês** (renova no aniversário do
|
|
194
|
-
contrato).
|
|
195
|
-
|
|
196
|
-
`FaceGateError.code`:
|
|
197
|
-
|
|
198
|
-
| code | quando |
|
|
199
|
-
|---|---|
|
|
200
|
-
| `token_window_limit` | janela de tokens esgotada (`resetsAt` = quando renova) |
|
|
201
|
-
| `token_month_limit` | teto mensal atingido (`resetsAt` = data da renovação) |
|
|
202
|
-
| `forbidden` | serviço não contratado, chave bloqueada/expirada, fora do contrato ou IP não liberado |
|
|
203
|
-
| `unauthorized` | chave/ticket inválido ou expirado |
|
|
204
|
-
| `camera_denied` / `camera_unavailable` | permissão negada, sem câmera ou página sem HTTPS |
|
|
205
|
-
| `network` / `server` / `bad_request` | falhas de conexão ou da API |
|
|
206
|
-
|
|
207
|
-
## Segurança e LGPD
|
|
208
|
-
|
|
209
|
-
- A whitelist de IPs da chave vale para o **seu servidor** (onde o ticket é emitido); o navegador usa só o
|
|
210
|
-
ticket de uso único.
|
|
211
|
-
- A API não guarda as imagens: só o resultado e metadados da validação.
|
|
212
|
-
- A câmera exige HTTPS (ou `localhost`).
|
|
213
|
-
- As chamadas do servidor saem com o User-Agent `face-gate-sdk/<versão> (node <versão>; <sistema>)`
|
|
214
|
-
(`SDK_USER_AGENT`). Assim, uma tentativa de um IP não liberado aparece no portal como "SDK Face Gate <versão>".
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
© BelTec, do grupo Belfort. Uso sujeito ao contrato do Face Gate.
|
|
1
|
+
# @beltec/face-gate
|
|
2
|
+
|
|
3
|
+
**Face Gate, uma solução BelTec** — validação de fotos de rosto para o cadastro de visitantes e moradores no
|
|
4
|
+
controle de acesso. Este pacote traz:
|
|
5
|
+
|
|
6
|
+
- **`<FaceGate>`** (React): webcam com validação em **tempo real** — mira que segue o rosto, orientação do que
|
|
7
|
+
corrigir, captura automática só quando a pessoa fica parada com tudo aprovado, scanner sobre a foto e a foto
|
|
8
|
+
final (com o fundo removido, se contratado).
|
|
9
|
+
- **`@beltec/face-gate/server`** (Node/Next): guarda a sua chave, emite o ticket do navegador e valida arquivos.
|
|
10
|
+
|
|
11
|
+
O `client_secret` **nunca** vai ao navegador: o seu servidor pede um ticket de uso único (60 s) e só ele chega à
|
|
12
|
+
página.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
navegador ──POST /api/face-gate/ticket──▶ seu servidor ──chave──▶ Face Gate API
|
|
16
|
+
navegador ◀──────── ticket (60 s) ────────┘
|
|
17
|
+
navegador ══ WebSocket (ticket) ══▶ Face Gate API (quadros ao vivo → captura → resultado)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Instalação
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @beltec/face-gate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
React 18+ no navegador; Node 18.17+ no servidor. Crie a chave no portal do Face Gate
|
|
27
|
+
(**Chaves de acesso**): você recebe o `client_id` e o `client_secret` (este aparece uma vez só).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# .env do seu servidor
|
|
31
|
+
FACE_GATE_CLIENT_ID=...
|
|
32
|
+
FACE_GATE_CLIENT_SECRET=...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Next.js (App Router)
|
|
36
|
+
|
|
37
|
+
**1. Rota que emite o ticket** — `app/api/face-gate/ticket/route.ts`
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createFaceGate } from "@beltec/face-gate/server";
|
|
41
|
+
|
|
42
|
+
const faceGate = createFaceGate({
|
|
43
|
+
clientId: process.env.FACE_GATE_CLIENT_ID!,
|
|
44
|
+
clientSecret: process.env.FACE_GATE_CLIENT_SECRET!,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Só usuários logados no SEU sistema abrem a câmera (cada captura consome tokens do seu plano).
|
|
48
|
+
export const POST = faceGate.ticketHandler({
|
|
49
|
+
authorize: async (req) => Boolean(await getSessionFromYourApp(req)),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**2. A tela** — um Client Component
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
"use client";
|
|
57
|
+
import { FaceGate } from "@beltec/face-gate";
|
|
58
|
+
import "@beltec/face-gate/styles.css";
|
|
59
|
+
|
|
60
|
+
export function CadastroFoto() {
|
|
61
|
+
return (
|
|
62
|
+
<FaceGate
|
|
63
|
+
clean // devolve a foto sem fundo (serviço "validar e remover fundo" contratado)
|
|
64
|
+
getTicket={async () => {
|
|
65
|
+
const r = await fetch("/api/face-gate/ticket", { method: "POST" });
|
|
66
|
+
if (!r.ok) throw new Error("Não foi possível abrir a câmera");
|
|
67
|
+
return r.json();
|
|
68
|
+
}}
|
|
69
|
+
onCapture={({ image, result }) => {
|
|
70
|
+
if (result.accepted) salvarFoto(image); // data URL (PNG sem fundo ou JPEG do quadro)
|
|
71
|
+
}}
|
|
72
|
+
onError={(e) => console.warn(e.code, e.message)}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Validar um arquivo (sem webcam)
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createFaceGate } from "@beltec/face-gate/server";
|
|
82
|
+
|
|
83
|
+
const faceGate = createFaceGate({ clientId, clientSecret });
|
|
84
|
+
const result = await faceGate.validate(fileBuffer, { clean: true, model: "birefnet-portrait" });
|
|
85
|
+
|
|
86
|
+
result.accepted; // true | false
|
|
87
|
+
result.hard_fails; // ex.: ["eyes_closed", "glasses_sunglasses"]
|
|
88
|
+
result.cleaned_image; // PNG sem fundo (data URL), quando clean
|
|
89
|
+
result.usage?.tokens; // tokens gastos nesta análise
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Com melhoria de imagem: `faceGate.validate(fileBuffer, { enhance: "classic" })` (veja abaixo).
|
|
93
|
+
|
|
94
|
+
Express/Fastify funcionam igual: use `createStreamTicket()` na sua rota e devolva `{ ticket, url }` ao navegador.
|
|
95
|
+
|
|
96
|
+
## `<FaceGate>` — propriedades
|
|
97
|
+
|
|
98
|
+
| Prop | Tipo | Padrão | |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `getTicket` | `() => Promise<{ ticket, url }>` | — | chama a sua rota de ticket |
|
|
101
|
+
| `onCapture` | `({ image, frame, result }) => void` | — | foto capturada e validada |
|
|
102
|
+
| `onLive` | `(result) => void` | — | cada quadro analisado ao vivo |
|
|
103
|
+
| `onError` | `(error: FaceGateError) => void` | — | câmera negada, limite de tokens, rede… |
|
|
104
|
+
| `clean` | `boolean` | `false` | remover o fundo da foto capturada |
|
|
105
|
+
| `model` | `"u2net" \| "u2netp" \| "birefnet-portrait"` | servidor | modelo da remoção de fundo |
|
|
106
|
+
| `device` | `"cpu" \| "gpu"` | servidor | onde rodar a remoção de fundo |
|
|
107
|
+
| `enhance` | `"classic" \| "gfpgan" \| null` | `null` | melhorar a foto capturada antes de validar (serviço "validar com melhoria") |
|
|
108
|
+
| `thresholds` | `Record<string, number>` | servidor | limiares (nomes de `GET /v1/thresholds`) |
|
|
109
|
+
| `autoFrame` | `boolean` | `true` | enquadramento automático: a imagem aproxima e acompanha o rosto, mantendo-o no centro; a foto capturada é esse recorte e só sai com o rosto centralizado (tolerância de 8%, ou a sua em `thresholds` se for mais rígida). Ignora as tarjas pretas que algumas câmeras acrescentam |
|
|
110
|
+
| `mirror` | `boolean` | `true` | prévia espelhada, como um espelho (a foto enviada e a final não são espelhadas) |
|
|
111
|
+
| `autoStart` | `boolean` | `false` | ligar a câmera ao montar |
|
|
112
|
+
| `intro` | `boolean` | `true` | abertura animada do escudo |
|
|
113
|
+
| `watermark` | `boolean` | `true` | escudo no canto do vídeo |
|
|
114
|
+
| `labels` | `Partial<FaceGateLabels>` | pt-BR | troque qualquer texto |
|
|
115
|
+
|
|
116
|
+
### Aparência
|
|
117
|
+
|
|
118
|
+
Os estilos usam o prefixo `.fg-` e variáveis CSS:
|
|
119
|
+
|
|
120
|
+
```css
|
|
121
|
+
.minha-tela .fg-root {
|
|
122
|
+
--fg-accent: #c9a43e; /* mira, botão, barra de "fique parado" */
|
|
123
|
+
--fg-accent-text: #1d2125; /* texto sobre o botão */
|
|
124
|
+
--fg-surface: #1d2125; /* fundo do vídeo */
|
|
125
|
+
--fg-radius: 12px;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Respeita `prefers-reduced-motion` (sem animações).
|
|
130
|
+
|
|
131
|
+
## Melhoria de imagem
|
|
132
|
+
|
|
133
|
+
Serviço contratável **"Validar com melhoria"** (scope `validate_enhance`). A API melhora a foto, valida a
|
|
134
|
+
versão melhorada e devolve `enhancement` com o resultado **antes** (original) e **depois** (melhorada) e a
|
|
135
|
+
imagem melhorada (`enhancement.enhanced_image`, JPEG). No `<FaceGate enhance="classic">`, só a foto
|
|
136
|
+
capturada passa pela melhoria (os quadros ao vivo não), o `onCapture` entrega a melhorada em `image` e a
|
|
137
|
+
tela mostra o selo "Imagem melhorada".
|
|
138
|
+
|
|
139
|
+
| Método | O que faz | Custo |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `classic` | balanço de branco, exposição, contraste local, redução de ruído e nitidez leve. Determinística, não altera o rosto | baixo |
|
|
142
|
+
| `gfpgan` | a clássica + restauração do rosto com IA (GFPGAN). Recria detalhes do rosto: a resposta traz o aviso `face_restored_ai` e o componente mostra o alerta | alto |
|
|
143
|
+
|
|
144
|
+
**Regra:** a melhoria só corrige luz (`poor_lighting`, `overexposed`) e rosto não encontrado por falta de
|
|
145
|
+
luz (`no_face`). As demais falhas da foto original — desfoque, olhos fechados, óculos escuros, pose… —
|
|
146
|
+
continuam reprovando (`enhancement.carried_over`). Com `clean`, o fundo é removido da foto melhorada (exige
|
|
147
|
+
também o serviço "validar e remover fundo").
|
|
148
|
+
|
|
149
|
+
**Tokens:** a validação roda duas vezes (antes e depois) e soma a etapa `usage.breakdown.enhance`
|
|
150
|
+
(a clássica é barata; o GFPGAN custa bem mais).
|
|
151
|
+
|
|
152
|
+
## Avatar
|
|
153
|
+
|
|
154
|
+
Serviço **Avatar** contratado (`POST /v1/photos/avatar`): valida a foto e, se aprovada, devolve o avatar só com a
|
|
155
|
+
pessoa do rosto em foco — o fundo e outros elementos ou pessoas são removidos —, recortado centrado no rosto.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { createFaceGate } from "@beltec/face-gate/server";
|
|
159
|
+
|
|
160
|
+
const faceGate = createFaceGate({ clientId, clientSecret });
|
|
161
|
+
const r = await faceGate.avatar(fileBuffer, {
|
|
162
|
+
shape: "round", // "round" (padrão; fora do círculo transparente) | "square"
|
|
163
|
+
size: 512, // 64 a 1024
|
|
164
|
+
backgroundColor: "#1d252a", // ou background: <imagem> (tem prioridade); sem nenhum = PNG transparente
|
|
165
|
+
margin: 25, // só com fundo: % do quadro deixado para o fundo (0 a 60)
|
|
166
|
+
layout: "bottom", // só com fundo: "center" | "bottom" | "left" | "right"
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (r.accepted) r.avatar_image; // data:image/png;base64,...
|
|
170
|
+
else r.hard_fails; // sem avatar: os mesmos motivos da validação
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| Opção | Tipo | Padrão | |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `shape` | `"round" \| "square"` | `"round"` | formato do recorte |
|
|
176
|
+
| `size` | `number` | `512` | lado em pixels (64 a 1024) |
|
|
177
|
+
| `backgroundColor` | `string` | — | cor sólida `#RRGGBB` |
|
|
178
|
+
| `background` | `Blob \| ArrayBuffer \| Uint8Array` | — | imagem de fundo (preenche o quadro) |
|
|
179
|
+
| `template` | `number` | — | id de um **template de avatar** salvo no portal (Avatares): formato, tamanho, fundo e enquadramento prontos, sem reenviar a imagem; as outras opções têm prioridade |
|
|
180
|
+
| `backgroundPosition` | `{ x?, y?, zoom? }` | centro, `zoom` 100 | **só com `background`**: qual parte da imagem aparece — `x`/`y` de 0 a 100 (50 = centro, como o `object-position` do CSS) e `zoom` de 100 a 300 % |
|
|
181
|
+
| `margin` | `number` | `0` | **só com fundo**: % do quadro deixado para o fundo em volta da pessoa (0 a 60) |
|
|
182
|
+
| `layout` | `"center" \| "bottom" \| "left" \| "right"` | `"center"` | **só com fundo**: onde a pessoa fica (centro, apoiada na base, base à esquerda/direita) |
|
|
183
|
+
| `model` / `device` | | servidor (BiRefNet) | modelo de remoção de fundo que separa a pessoa |
|
|
184
|
+
| `thresholds` | `Record<string, number>` | servidor | limiares da validação |
|
|
185
|
+
|
|
186
|
+
O resultado traz também `avatar` (`shape`, `size`, `model`, `background`, `margin`, `layout`). **Tokens:** soma a etapa `avatar`
|
|
187
|
+
em `usage.breakdown` (a separação da pessoa roda na imagem analisada inteira).
|
|
188
|
+
|
|
189
|
+
## Tokens e erros
|
|
190
|
+
|
|
191
|
+
Cada foto capturada (e cada `validate`) consome **tokens** do seu plano — megapixels processados × peso de cada
|
|
192
|
+
etapa; foto maior ou remoção de fundo com BiRefNet gastam mais. Os quadros ao vivo da webcam não consomem.
|
|
193
|
+
O plano limita os tokens por **janela** (ex.: 5 h a partir do 1º uso) e por **mês** (renova no aniversário do
|
|
194
|
+
contrato).
|
|
195
|
+
|
|
196
|
+
`FaceGateError.code`:
|
|
197
|
+
|
|
198
|
+
| code | quando |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `token_window_limit` | janela de tokens esgotada (`resetsAt` = quando renova) |
|
|
201
|
+
| `token_month_limit` | teto mensal atingido (`resetsAt` = data da renovação) |
|
|
202
|
+
| `forbidden` | serviço não contratado, chave bloqueada/expirada, fora do contrato ou IP não liberado |
|
|
203
|
+
| `unauthorized` | chave/ticket inválido ou expirado |
|
|
204
|
+
| `camera_denied` / `camera_unavailable` | permissão negada, sem câmera ou página sem HTTPS |
|
|
205
|
+
| `network` / `server` / `bad_request` | falhas de conexão ou da API |
|
|
206
|
+
|
|
207
|
+
## Segurança e LGPD
|
|
208
|
+
|
|
209
|
+
- A whitelist de IPs da chave vale para o **seu servidor** (onde o ticket é emitido); o navegador usa só o
|
|
210
|
+
ticket de uso único.
|
|
211
|
+
- A API não guarda as imagens: só o resultado e metadados da validação.
|
|
212
|
+
- A câmera exige HTTPS (ou `localhost`).
|
|
213
|
+
- As chamadas do servidor saem com o User-Agent `face-gate-sdk/<versão> (node <versão>; <sistema>)`
|
|
214
|
+
(`SDK_USER_AGENT`). Assim, uma tentativa de um IP não liberado aparece no portal como "SDK Face Gate <versão>".
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
© BelTec, do grupo Belfort. Uso sujeito ao contrato do Face Gate.
|
package/dist/index.cjs
CHANGED
|
@@ -55,6 +55,8 @@ var CLOSE = {
|
|
|
55
55
|
4429: ["token_window_limit", "Limite de uso atingido. Aguarde e tente de novo."],
|
|
56
56
|
1011: ["server", "Erro interno na API."]
|
|
57
57
|
};
|
|
58
|
+
var DEFAULT_FPS = 6;
|
|
59
|
+
var MAX_BUFFERED = 32 * 1024;
|
|
58
60
|
function openFaceGateStream(url, ticket, options, handlers, WS = WebSocket) {
|
|
59
61
|
const ws = new WS(url);
|
|
60
62
|
ws.binaryType = "arraybuffer";
|
|
@@ -65,7 +67,7 @@ function openFaceGateStream(url, ticket, options, handlers, WS = WebSocket) {
|
|
|
65
67
|
ws.onmessage = (e) => {
|
|
66
68
|
if (typeof e.data !== "string") return;
|
|
67
69
|
const m = JSON.parse(e.data);
|
|
68
|
-
if (m.type === "ready") handlers.onReady?.();
|
|
70
|
+
if (m.type === "ready") handlers.onReady?.(Number(m.max_fps) || DEFAULT_FPS);
|
|
69
71
|
else if (m.type === "frame") handlers.onFrame?.(m.seq, m.result, m.stable ?? { ms: 0, needed: 0 });
|
|
70
72
|
else if (m.type === "captured") handlers.onCaptured?.(m.seq);
|
|
71
73
|
else if (m.type === "final") {
|
|
@@ -81,7 +83,7 @@ function openFaceGateStream(url, ticket, options, handlers, WS = WebSocket) {
|
|
|
81
83
|
const open = () => ws.readyState === 1;
|
|
82
84
|
return {
|
|
83
85
|
sendFrame: (frame) => {
|
|
84
|
-
if (!open()) return null;
|
|
86
|
+
if (!open() || ws.bufferedAmount > MAX_BUFFERED) return null;
|
|
85
87
|
ws.send(frame);
|
|
86
88
|
return ++seq;
|
|
87
89
|
},
|
|
@@ -171,6 +173,13 @@ function cropTransform(c, mirror = true) {
|
|
|
171
173
|
const move = `translate(${-c.x * 100}%, ${-c.y * 100}%)`;
|
|
172
174
|
return mirror ? `translateX(100%) scale(${-k}, ${k}) ${move}` : `scale(${k}) ${move}`;
|
|
173
175
|
}
|
|
176
|
+
function overlayTransform(used, now, mirror = true) {
|
|
177
|
+
const k = used.s / now.s;
|
|
178
|
+
const dx = (used.x - now.x) / now.s;
|
|
179
|
+
const tx = mirror ? 1 - k - dx : dx;
|
|
180
|
+
const ty = (used.y - now.y) / now.s;
|
|
181
|
+
return `translate(${tx * 100}%, ${ty * 100}%) scale(${k})`;
|
|
182
|
+
}
|
|
174
183
|
function boxStyle(b, mirror) {
|
|
175
184
|
const left = mirror ? 1 - b.xmax : b.xmin;
|
|
176
185
|
return {
|
|
@@ -300,6 +309,12 @@ function FaceGate({
|
|
|
300
309
|
const canvasRef = (0, import_react2.useRef)(null);
|
|
301
310
|
const framesRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
|
|
302
311
|
const viewRef = (0, import_react2.useRef)(null);
|
|
312
|
+
const overlayRef = (0, import_react2.useRef)(null);
|
|
313
|
+
const overlayCropRef = (0, import_react2.useRef)(FULL);
|
|
314
|
+
const fpsRef = (0, import_react2.useRef)(6);
|
|
315
|
+
const sendRef = (0, import_react2.useRef)(() => {
|
|
316
|
+
});
|
|
317
|
+
const lastSeqRef = (0, import_react2.useRef)(0);
|
|
303
318
|
const cropRef = (0, import_react2.useRef)(FULL);
|
|
304
319
|
const targetRef = (0, import_react2.useRef)(FULL);
|
|
305
320
|
const areaRef = (0, import_react2.useRef)(WHOLE);
|
|
@@ -334,6 +349,7 @@ function FaceGate({
|
|
|
334
349
|
const tick = () => {
|
|
335
350
|
cropRef.current = ease(cropRef.current, targetRef.current);
|
|
336
351
|
if (viewRef.current) viewRef.current.style.transform = cropTransform(cropRef.current, mirror);
|
|
352
|
+
if (overlayRef.current) overlayRef.current.style.transform = overlayTransform(overlayCropRef.current, cropRef.current, mirror);
|
|
337
353
|
id = requestAnimationFrame(tick);
|
|
338
354
|
};
|
|
339
355
|
id = requestAnimationFrame(tick);
|
|
@@ -456,7 +472,9 @@ function FaceGate({
|
|
|
456
472
|
gateRef.current = { camera: false, socket: false, intro: !playIntro };
|
|
457
473
|
framesRef.current.clear();
|
|
458
474
|
hadFaceRef.current = false;
|
|
459
|
-
|
|
475
|
+
lastSeqRef.current = 0;
|
|
476
|
+
cropRef.current = targetRef.current = overlayCropRef.current = FULL;
|
|
477
|
+
if (overlayRef.current) overlayRef.current.style.transform = "";
|
|
460
478
|
areaRef.current = WHOLE;
|
|
461
479
|
measuredRef.current = 0;
|
|
462
480
|
lostSinceRef.current = 0;
|
|
@@ -495,13 +513,17 @@ function FaceGate({
|
|
|
495
513
|
});
|
|
496
514
|
}
|
|
497
515
|
streamRef.current = openFaceGateStream(tk.value.url, tk.value.ticket, cfg.current.options, {
|
|
498
|
-
onReady: () => {
|
|
516
|
+
onReady: (maxFps) => {
|
|
517
|
+
fpsRef.current = Math.min(Math.max(maxFps, 1), 10);
|
|
499
518
|
gateRef.current.socket = true;
|
|
500
519
|
goLiveIfReady();
|
|
501
520
|
},
|
|
502
521
|
onFrame: (seq, r, st) => {
|
|
522
|
+
if (seq <= lastSeqRef.current) return;
|
|
523
|
+
lastSeqRef.current = seq;
|
|
503
524
|
const f = framesRef.current.get(seq);
|
|
504
525
|
if (f) f.result = r;
|
|
526
|
+
if (autoFrame) overlayCropRef.current = f?.crop ?? cropRef.current;
|
|
505
527
|
aim(seq, r.dominant_face_bbox);
|
|
506
528
|
const hasFace = Boolean(r.dominant_face_bbox);
|
|
507
529
|
if (hasFace && !hadFaceRef.current) setDetectKey((k) => k + 1);
|
|
@@ -509,7 +531,6 @@ function FaceGate({
|
|
|
509
531
|
setLive(r);
|
|
510
532
|
setStable(st);
|
|
511
533
|
cfg.current.onLive?.(r);
|
|
512
|
-
requestAnimationFrame(sendFrame);
|
|
513
534
|
},
|
|
514
535
|
onCaptured: (seq) => {
|
|
515
536
|
const frames = framesRef.current;
|
|
@@ -543,6 +564,14 @@ function FaceGate({
|
|
|
543
564
|
(0, import_react2.useEffect)(() => {
|
|
544
565
|
if (autoStart) void start();
|
|
545
566
|
}, []);
|
|
567
|
+
(0, import_react2.useEffect)(() => {
|
|
568
|
+
sendRef.current = sendFrame;
|
|
569
|
+
});
|
|
570
|
+
(0, import_react2.useEffect)(() => {
|
|
571
|
+
if (phase !== "live") return;
|
|
572
|
+
const id = setInterval(() => sendRef.current(), 1e3 / fpsRef.current);
|
|
573
|
+
return () => clearInterval(id);
|
|
574
|
+
}, [phase]);
|
|
546
575
|
const cameraOn = phase === "starting" || phase === "live";
|
|
547
576
|
const box = phase === "live" ? live?.dominant_face_bbox ?? null : null;
|
|
548
577
|
const ok = Boolean(live?.accepted);
|
|
@@ -584,10 +613,18 @@ function FaceGate({
|
|
|
584
613
|
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fg-search-sweep" })
|
|
585
614
|
}
|
|
586
615
|
),
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
616
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
617
|
+
"div",
|
|
618
|
+
{
|
|
619
|
+
ref: overlayRef,
|
|
620
|
+
"aria-hidden": "true",
|
|
621
|
+
style: { position: "absolute", inset: 0, transformOrigin: "0 0", pointerEvents: "none" },
|
|
622
|
+
children: box && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "fg-track", "data-state": ok ? "ok" : "warn", style: boxStyle(box, mirror), children: [
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fg-track-line" }),
|
|
624
|
+
holding && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fg-track-hold", style: { transform: `scaleX(${progress})` } })
|
|
625
|
+
] }, detectKey)
|
|
626
|
+
}
|
|
627
|
+
),
|
|
591
628
|
phase === "processing" && frozen && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
592
629
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: frozen.url, alt: "", className: "fg-video", style: mirror ? { transform: "scaleX(-1)" } : void 0 }),
|
|
593
630
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fg-scan", style: boxStyle(frozen.box, mirror), "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fg-scan-line" }) })
|