mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +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.
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import type { Payload } from 'payload'
|
|
|
|
import { ARCHIVE_PHOTO_SLUG, type ArchivePhotoImport } from '@/lib/archivePhoto'
|
|
import { FACE_INDEX_CONSENT_KEY } from '@/lib/campaignConsentKeys'
|
|
import { FACE_SEARCH_DESCRIPTOR_LENGTH } from '@/lib/faceSearch'
|
|
import { getConsentByKey } from '@/utilities/campaignConsent'
|
|
import { enrollFaceSubject } from '@/utilities/faceSubjects/faceSubjectEnrollment'
|
|
import { ingestArchivePhoto } from '@/utilities/flickr/archivePhotoIngest'
|
|
|
|
import { ARCHIVE_PHOTO_JPEG_BYTES } from './archivePhotoFixture'
|
|
|
|
/**
|
|
* C234 — the fixtures shared by the face-search int specs (subject/index and
|
|
* the API): the file-backed archive photo over the real Payload test database
|
|
* and the enrolled subject. The two spec files serialize on the same lease
|
|
* because the index revision is derived state over EVERY enrolled subject — an
|
|
* enrollment in the sibling spec changes the key and would legitimately mark
|
|
* photos stale mid-assertion.
|
|
*/
|
|
export const FACE_SPECS_LEASE_KEY = 'c234-face-specs'
|
|
export const FACE_INDEX_LEASE_KEY = 'c234-face-index-consent'
|
|
export const FACE_SEARCH_LEASE_KEY = 'c234-face-search-consent'
|
|
|
|
export const faceTestVector = (value: number): number[] =>
|
|
Array.from({ length: FACE_SEARCH_DESCRIPTOR_LENGTH }, () => value)
|
|
|
|
const record = (
|
|
flickrId: string,
|
|
overrides: Partial<ArchivePhotoImport> = {},
|
|
): ArchivePhotoImport => ({
|
|
flickrId,
|
|
sourceUrl: `https://www.flickr.com/photos/depjorgesolla/${flickrId}/`,
|
|
owner: '12345678@N00',
|
|
license: '0',
|
|
title: `Foto ${flickrId}`,
|
|
description: null,
|
|
tags: [],
|
|
takenAt: '2025-03-14 10:20:30',
|
|
postedAt: '2025-03-15T10:00:00.000Z',
|
|
albums: [],
|
|
geo: null,
|
|
exif: [],
|
|
originalUrl: `https://live.staticflickr.com/65535/${flickrId}_abcdef_o.jpg`,
|
|
originalKind: 'original',
|
|
...overrides,
|
|
})
|
|
|
|
/**
|
|
* Ingests one archive photo fixture and optionally approves it (approval needs
|
|
* the album's removal channel to be configured by the caller).
|
|
*/
|
|
export const createFaceTestPhoto = async ({
|
|
payload,
|
|
approved,
|
|
prefix,
|
|
trackedFlickrIds,
|
|
tempDirs,
|
|
}: {
|
|
payload: Payload
|
|
approved: boolean
|
|
prefix: string
|
|
trackedFlickrIds: Set<string>
|
|
tempDirs: string[]
|
|
}): Promise<{ id: number; flickrId: string }> => {
|
|
const flickrId = `${prefix}-${randomUUID().slice(0, 8)}`
|
|
trackedFlickrIds.add(flickrId)
|
|
const dir = await mkdtemp(join(tmpdir(), `${prefix}-photo-`))
|
|
tempDirs.push(dir)
|
|
const filePath = join(dir, `flickr-${flickrId}.jpg`)
|
|
await writeFile(filePath, ARCHIVE_PHOTO_JPEG_BYTES)
|
|
const created = await ingestArchivePhoto(payload, record(flickrId), { filePath })
|
|
if (created.status !== 'created') throw new Error(`fixture not created: ${created.status}`)
|
|
|
|
if (approved) {
|
|
await payload.update({
|
|
collection: ARCHIVE_PHOTO_SLUG,
|
|
id: created.id,
|
|
data: { publicationStatus: 'approved' },
|
|
overrideAccess: true,
|
|
})
|
|
}
|
|
return { id: created.id, flickrId }
|
|
}
|
|
|
|
/** Enrolls a subject with the current index Consent resolved by stable key. */
|
|
export const enrollFaceTestSubject = async ({
|
|
payload,
|
|
label,
|
|
vector,
|
|
trackedSubjectIds,
|
|
}: {
|
|
payload: Payload
|
|
label: string
|
|
vector: number[]
|
|
trackedSubjectIds: Set<number>
|
|
}): Promise<number> => {
|
|
const consent = await getConsentByKey(payload, FACE_INDEX_CONSENT_KEY)
|
|
if (!consent) throw new Error('index consent not configured')
|
|
|
|
const enrolled = await enrollFaceSubject({
|
|
payload,
|
|
label,
|
|
descriptor: vector,
|
|
consent: { id: consent.id, contentHash: consent.contentHash },
|
|
})
|
|
trackedSubjectIds.add(enrolled.id)
|
|
return enrolled.id
|
|
}
|