mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:25:20 +00:00
- /fotos/encontre + POST /api/fotos/selfie: só o vetor 128-d, sem imagem, sem score e sem nomes de terceiros; fail-closed pela flag photoAlbum.selfieSearchEnabled (default false) e pelos Consents por chave estável (busca-selfie-fotos/busca-selfie-indice, não seedados — a superfície nasce fechada até o aval jurídico/DPIA). - Índice A/C do gate (PR #1370): só quem aderiu é indexado; descriptors do acervo são transitórios. Collection faceSubject (admin-only, sem vínculo com Contact), vínculos matchedPhotos e marker faces.checkedKey no archivePhoto (modelo + enrolledAt). - Engine @vladmandic/face-api nos dois lados: browser lazy com modelos same-origin (copy-face-vision-assets no prebuild/predev/Docker) e CLI node-wasm sem tfjs-node. CLIs ops-only faces:enroll / faces:index --apply|--verify com guardas C231/C232. - Opt-out self-service por reconhecimento (leave-index) + canal de remoção do C233; rate limit próprio (namespace selfie:<hash>) e bounded body extraído do beacon. - UI portada classe-a-classe do design estendido (paridade certificada, tier primário); e2e frontendFotosSelfie (manifest+curado+projeto, engine stubada) e canário do engine real; runbook §C234, contrato em AGENTS-public.md e changelog. - Migration aditiva add_face_subject (SQL revisado); hard-stops aprovados na sessão.
91 lines
3.8 KiB
JavaScript
91 lines
3.8 KiB
JavaScript
/**
|
|
* C234 — pure planning/reporting of `pnpm faces:enroll`: the argv parser and
|
|
* the human lines + JSON receipt of the enrollment. No I/O of its own — the
|
|
* engine, the Consent resolution and the Payload write arrive as parameters,
|
|
* so the unit spec drives everything with fakes.
|
|
*/
|
|
|
|
const USAGE =
|
|
'uso: `pnpm faces:enroll --label "<nome>" --selfie <arquivo> [--subject <id>] [--apply]`'
|
|
|
|
export const FACE_ENROLL_DEFAULT_OUT_DIR = 'data/face'
|
|
|
|
export const faceEnrollReportStamp = (runAt) => String(runAt).replace(/[:.]/g, '-')
|
|
|
|
/**
|
|
* `pnpm faces:enroll` argv parser — pure, so the unit spec pins it (the CLI
|
|
* owns the exit and the help text). The default mode is the plan/dry-run;
|
|
* `--label` and `--selfie` are required in both modes so the plan shows the
|
|
* real operation, not an abstract one.
|
|
*
|
|
* @param {string[]} [argv]
|
|
* @returns {{ apply: boolean, label: string | null, selfie: string | null, subject: number | null, out: string, help: boolean }}
|
|
*/
|
|
export const parseFaceEnrollCliArgs = (argv = process.argv.slice(2)) => {
|
|
const options = {
|
|
apply: false,
|
|
label: null,
|
|
selfie: null,
|
|
subject: null,
|
|
out: FACE_ENROLL_DEFAULT_OUT_DIR,
|
|
help: false,
|
|
}
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index]
|
|
const value = () => {
|
|
const next = argv[++index]
|
|
if (next === undefined || next.startsWith('--')) {
|
|
throw new Error(`faltou valor para ${arg} — ${USAGE}.`)
|
|
}
|
|
return next
|
|
}
|
|
|
|
if (arg === '--help' || arg === '-h') options.help = true
|
|
else if (arg === '--apply') options.apply = true
|
|
else if (arg === '--label') options.label = value()
|
|
else if (arg === '--selfie') options.selfie = value()
|
|
else if (arg === '--subject') options.subject = Number(value())
|
|
else if (arg === '--out') options.out = value()
|
|
else throw new Error(`argumento desconhecido: ${arg} — ${USAGE}.`)
|
|
}
|
|
|
|
if (options.help) return options
|
|
if (!options.label?.trim()) throw new Error(`--label é obrigatório — ${USAGE}.`)
|
|
if (!options.selfie?.trim()) throw new Error(`--selfie é obrigatório — ${USAGE}.`)
|
|
if (options.subject !== null && (!Number.isSafeInteger(options.subject) || options.subject < 1)) {
|
|
throw new Error('--subject deve ser um id >= 1.')
|
|
}
|
|
if (options.out.split(/[\\/]/).includes('..')) {
|
|
throw new Error('--out não pode escapar do diretório do repo (sem "..").')
|
|
}
|
|
return options
|
|
}
|
|
|
|
/**
|
|
* The receipt + human lines of one enrollment: what was detected (face count —
|
|
* never the descriptor), the subject row and the consent snapshot. A refusal
|
|
* (0 or >1 faces) is a report with `allowed: false` and its reason.
|
|
*
|
|
* @param {{ runAt: string, mode: string, target: string, model: string, label: string, subjectId: number | null, subjectAction: 'created' | 'updated' | null, faceCount: number, allowed: boolean, reason?: string | null, consentKey: string, consentHash: string | null, durationMs: number }} report
|
|
*/
|
|
export const formatFaceEnrollReport = (report) => {
|
|
const lines = [
|
|
`[faces:enroll] modo: ${report.mode} | alvo: ${report.target} | modelo: ${report.model}`,
|
|
`[faces:enroll] selfie: ${report.faceCount} rosto(s) detectado(s)`,
|
|
]
|
|
if (!report.allowed) {
|
|
lines.push(`[faces:enroll] RECUSADO: ${report.reason ?? 'selfie fora do contrato'}`)
|
|
return lines
|
|
}
|
|
if (report.subjectAction === null) {
|
|
lines.push(`[faces:enroll] plano: ${report.label} (nada gravado; confirme com --apply)`)
|
|
return lines
|
|
}
|
|
lines.push(
|
|
`[faces:enroll] ${report.subjectAction === 'created' ? 'criada' : 'atualizada'}: #${report.subjectId} (${report.label})`,
|
|
`[faces:enroll] consentimento: ${report.consentKey} · hash ${report.consentHash ?? '(ausente)'}`,
|
|
`[faces:enroll] próximo passo: pnpm faces:index --apply (reprocessa as fotos aprovadas)`,
|
|
)
|
|
return lines
|
|
}
|