# API Face Review: riepilogo completo degli endpoint ## Scopo generale Le API Face Review gestiscono il flusso completo di revisione dei risultati del riconoscimento facciale: - lettura delle run HDBSCAN; - visualizzazione dei cluster automatici; - visualizzazione dei volti classificati come noise; - generazione protetta delle miniature dei volti; - creazione e rinomina delle persone; - assegnazione di uno o più cluster alla stessa persona; - esclusione di singoli volti da una persona; - assegnazione singola o multipla dei volti non raggruppati; - conservazione separata del risultato automatico e della decisione manuale. Principio fondamentale: ```text cluster automatico != persona confermata ``` Più cluster possono essere collegati allo stesso `person_id` senza fondere o cancellare i cluster originali. ## Prefisso e autenticazione Tutti gli endpoint descritti usano il prefisso: ```text /face-review ``` Le route sono protette dal middleware JWT globale. Il client deve inviare: ```http Authorization: Bearer ``` Il middleware JWT verifica il token e salva il payload in: ```javascript req.user ``` Il nome dell'utente autenticato è disponibile in: ```javascript req.user.name ``` ### Risoluzione dell'utente visualizzato La funzione `resolveFaceReviewUser(req)` applica queste regole: - un utente normale senza parametro `user` vede esclusivamente i propri dati; - Admin può specificare `?user=Fabio` per visualizzare la libreria di un altro utente; - un utente normale non può selezionare un altro utente; - cluster, volti, fotografie e persone vengono sempre verificati lato backend. Esempi: ```text JWT name = Fabio GET /face-review/context => target_user = Fabio JWT name = Admin GET /face-review/context?user=Fabio => target_user = Fabio JWT name = Fabio GET /face-review/context?user=Jessica => HTTP 403 ``` ## Endpoint di sola lettura ### GET /face-review/context Restituisce il contesto di autenticazione e la run completata più recente dell'utente visualizzato. Richiesta utente normale: ```http GET /face-review/context Authorization: Bearer ``` Richiesta Admin: ```http GET /face-review/context?user=Fabio Authorization: Bearer ``` Risposta: ```json { "authenticated_user": "Fabio", "target_user": "Fabio", "is_admin": false, "can_select_user": false, "latest_run_id": "c44546a7-55a8-423c-b3b3-7dd5da671a1d" } ``` Possibili errori: - `401` se il token non è valido o manca l'identità; - `403` se un utente normale prova a selezionare un altro utente; - `404` se l'utente richiesto non possiede dati riconoscibili. ### GET /face-review/runs Restituisce la cronologia delle run di clustering per l'utente visualizzato. ```http GET /face-review/runs GET /face-review/runs?user=Fabio ``` Ogni run contiene, tra gli altri: - `id`; - `user`; - `status`; - `scope`; - `embedding_count`; - `min_cluster_size`; - `min_samples`; - `metric`; - `selection_method`; - `engine` e `engine_version`; - `cluster_count`; - `clustered_faces`; - `noise_faces`; - `noise_percent`; - date di avvio e completamento. Risposta: ```json { "user": "Fabio", "runs": [ { "id": "...", "status": "done", "embedding_count": 30, "cluster_count": 5, "clustered_faces": 14, "noise_faces": 16 } ] } ``` ### GET /face-review/clusters Restituisce i cluster e i relativi volti. Senza `run_id` usa la run completata più recente. ```http GET /face-review/clusters GET /face-review/clusters?run_id= GET /face-review/clusters?user=Fabio ``` Per ogni cluster restituisce: - identificativo e label HDBSCAN; - dimensione; - parametri della run; - eventuale `person_id` e nome della persona; - elenco dei volti appartenenti al cluster. Ogni volto include: - `id`; - `photo_id` e `photo_name`; - `face_index`; - detector score; - probabilità HDBSCAN; - stato di revisione; - rotazione; - URL protetto della miniatura. Risposta sintetica: ```json { "user": "Fabio", "run": { "id": "...", "status": "done" }, "clusters": [ { "id": "...", "label": 2, "size": 4, "person_id": "...", "person_name": "Jessica", "faces": [] } ] } ``` ### GET /face-review/clusters/:clusterId Restituisce un singolo cluster, purché appartenga all'utente autorizzato. ```http GET /face-review/clusters/ ``` Restituisce i dati del cluster e tutti i suoi volti. Risponde `404` se il cluster non esiste o appartiene a un altro utente. ### GET /face-review/unclustered Restituisce i volti noise della run più recente che non sono ancora assegnati manualmente a una persona. La query applica concettualmente: ```sql m.noise_flag = 1 AND f.person_id IS NULL ``` Richiesta: ```http GET /face-review/unclustered ``` Quando un noise viene assegnato a una persona: - `cluster_id` resta `NULL`; - `label` resta `-1`; - `noise_flag` resta `1`; - `person_id` viene valorizzato; - `review_status` diventa `accepted`; - il volto non compare più in questo endpoint. ### GET /face-review/persons Restituisce le persone dell'utente visualizzato. ```http GET /face-review/persons ``` Risposta: ```json { "user": "Fabio", "persons": [ { "id": "...", "user": "Fabio", "name": "Jessica", "created_at": "...", "updated_at": "..." } ] } ``` ### GET /face-review/persons/:personId Restituisce il dettaglio di una persona e tutti i volti assegnati, inclusi quelli provenienti da cluster e quelli assegnati manualmente dal noise. ```http GET /face-review/persons/ ``` La risposta include: - dati della persona; - numero totale di volti; - numero di cluster collegati; - elenco dei volti; - cluster di provenienza, se presente; - miniatura protetta. Un volto noise assegnato manualmente viene restituito con: ```text cluster_id = null cluster_label = null ``` ### GET /face-review/faces/:faceId/thumbnail Genera una miniatura WebP protetta del volto. ```http GET /face-review/faces//thumbnail GET /face-review/faces//thumbnail?size=256 ``` Dimensione predefinita: ```text 192 x 192 pixel ``` Intervallo accettato per `size`: ```text 64 <= size <= 512 ``` Pipeline dell'immagine: 1. recupera il volto e la fotografia filtrandoli per utente; 2. risolve il percorso tramite `resolveFilePathForEntryId()`; 3. applica esclusivamente `photos.rotation`; 4. non applica automaticamente l'orientamento EXIF; 5. aggiunge un margine al bounding box; 6. crea un crop quadrato; 7. ridimensiona in WebP. Header restituiti: ```http Content-Type: image/webp Cache-Control: private, max-age=3600 X-Content-Type-Options: nosniff ``` Il frontend deve caricare la miniatura tramite `fetch()` con Bearer token e trasformarla in Object URL, perché un normale elemento immagine non aggiunge automaticamente l'header Authorization. ## Endpoint di scrittura per le persone ### POST /face-review/persons Crea una nuova persona per l'utente visualizzato. ```http POST /face-review/persons Content-Type: application/json Authorization: Bearer ``` Body: ```json { "name": "Jessica" } ``` Comportamento: - normalizza spazi e nome; - rifiuta nomi vuoti; - limita il nome a 120 caratteri; - cerca una persona omonima senza distinguere maiuscole e minuscole; - se esiste già, restituisce la persona esistente; - altrimenti crea un UUID e inserisce il record. Codici: - `201` se la persona è stata creata; - `200` se il nome esisteva già; - `400` se il nome è assente o non valido. ### PATCH /face-review/persons/:personId Rinomina una persona. ```http PATCH /face-review/persons/ Content-Type: application/json Authorization: Bearer ``` Body: ```json { "name": "Nuovo nome" } ``` Comportamento: - verifica che la persona appartenga all'utente; - normalizza e valida il nome; - blocca duplicati dello stesso utente; - aggiorna `updated_at`; - non modifica volti o cluster. Possibili errori: - `400` nome non valido; - `404` persona non trovata; - `409` nome già utilizzato da un'altra persona. ### DELETE /face-review/persons/:personId/faces/:faceId Rimuove un volto da una persona senza cancellare il volto e senza cancellare il cluster automatico. ```http DELETE /face-review/persons//faces/ Authorization: Bearer ``` Operazioni transazionali: 1. verifica persona, volto e utente; 2. imposta `faces.person_id = NULL`; 3. mantiene `faces.cluster_id` invariato; 4. marca la membership come `excluded`; 5. salva `reviewed_by` e `reviewed_at`; 6. inserisce o aggiorna `person_face_exclusions`. Un volto rimosso da una persona può essere riassociato successivamente dal cluster o dalla UI appropriata. ## Endpoint di assegnazione cluster-persona ### POST /face-review/clusters/:clusterId/assign-person Assegna un cluster a una persona esistente e consente di includere o escludere singoli volti. ```http POST /face-review/clusters//assign-person Content-Type: application/json Authorization: Bearer ``` Body: ```json { "person_id": "", "included_face_ids": [ "face:...:0", "face:...:0" ] } ``` Comportamento transazionale: - verifica che persona e cluster appartengano allo stesso utente; - verifica che tutti gli ID inclusi appartengano al cluster; - per i volti inclusi: - imposta `faces.person_id`; - marca la membership come `accepted`; - rimuove eventuali esclusioni precedenti; - per i volti non inclusi: - non li assegna alla persona; - marca la membership come `excluded`; - registra `person_face_exclusions`; - imposta `face_clusters.person_id`. Questo endpoint permette di collegare più cluster alla stessa persona. Per esempio: ```text Cluster 2 -> Jessica Cluster 3 -> Jessica Cluster 4 -> Jessica ``` I cluster restano distinti e conservano la cronologia HDBSCAN. ## Endpoint di assegnazione dei noise ### POST /face-review/faces/:faceId/assign-person Assegna manualmente un singolo volto a una persona. ```http POST /face-review/faces//assign-person Content-Type: application/json Authorization: Bearer ``` Body: ```json { "person_id": "" } ``` Comportamento: - verifica persona, volto e utente; - aggiorna `faces.person_id`; - non modifica `faces.cluster_id`; - accetta la membership più recente del volto; - elimina l'eventuale esclusione precedente dalla stessa persona. Per un noise: ```text cluster_id resta NULL label resta -1 noise_flag resta 1 review_status diventa accepted ``` ### POST /face-review/unclustered/assign-person Assegna più volti noise selezionati alla stessa persona in una singola transazione. Persona esistente: ```json { "person_id": "", "face_ids": [ "face:...:0", "face:...:1" ] } ``` Nuova persona: ```json { "name": "Nuova persona", "face_ids": [ "face:...:0", "face:...:1" ] } ``` Controlli principali: - almeno un volto deve essere selezionato; - deve essere presente `person_id` oppure `name`; - usa la run completata più recente; - tutti i volti devono essere noise della run corrente; - tutti devono appartenere all'utente; - tutti devono avere `person_id IS NULL`; - se un solo volto non è valido, l'intera transazione viene annullata. Per ogni volto valido: - `faces.person_id` viene impostato; - `cluster_id` resta invariato, normalmente `NULL`; - la membership passa ad `accepted`; - vengono salvati revisore e data; - eventuali esclusioni pregresse dalla persona vengono eliminate. Risposta: ```json { "ok": true, "user": "Fabio", "person": { "id": "...", "name": "Jessica" }, "person_created": false, "run_id": "...", "assigned_face_ids": [], "assigned_count": 3 } ``` ## Tabelle coinvolte ### face_cluster_runs Conserva ogni esecuzione del clustering e i relativi parametri, metriche e stato. ### face_clusters Conserva i cluster automatici. Il campo `person_id` collega il cluster alla persona confermata senza eliminare il cluster. ### face_cluster_memberships Conserva l'appartenenza di ogni volto a una run e a un eventuale cluster. Campi rilevanti: - `run_id`; - `cluster_id`; - `face_id`; - `label`; - `probability`; - `noise_flag`; - `review_status`; - `reviewed_by`; - `reviewed_at`. Stati di revisione: ```text suggested accepted excluded ``` ### faces Conserva bounding box, embedding e stato corrente del volto. Distinzione importante: - `cluster_id` rappresenta il raggruppamento automatico corrente; - `person_id` rappresenta la persona confermata manualmente. ### persons Conserva le persone nominate, sempre separate per utente. ### person_face_exclusions Registra che uno specifico volto non appartiene a una specifica persona. Impedisce di perdere la decisione manuale durante successive revisioni. ## Regole di coerenza ### Volto clusterizzato e accettato ```text cluster_id = valorizzato person_id = valorizzato noise_flag = 0 review_status = accepted ``` ### Volto noise non revisionato ```text cluster_id = NULL person_id = NULL label = -1 noise_flag = 1 review_status = suggested ``` ### Volto noise assegnato manualmente ```text cluster_id = NULL person_id = valorizzato label = -1 noise_flag = 1 review_status = accepted ``` ### Volto escluso da una persona ```text person_id = NULL cluster_id = invariato review_status = excluded person_face_exclusions = riga presente ``` ## Transazioni e sicurezza Tutte le operazioni di assegnazione o rimozione importanti vengono eseguite in transazione. Se una validazione o una scrittura fallisce, viene eseguito il rollback completo. Le API non accettano percorsi filesystem dal browser. Le miniature partono dal solo `faceId`, recuperano la fotografia dal database e risolvono internamente il file. Ogni operazione verifica la coerenza tra: ```text utente della run utente della fotografia utente della persona utente autenticato o selezionato da Admin ``` ## Conversione dei segnaposto in Markdown standard Questo file usa segnaposto intenzionali: - `§1` al posto di intestazione livello 1; - `§2` al posto di intestazione livello 2; - `§3` al posto di intestazione livello 3; - ``` al posto della recinzione dei blocchi di codice; - ` al posto dell'apice rovescio inline. Esempio di conversione con `sed`, usando caratteri ASCII generati dalla shell: ```bash hash=$(printf '\043') tick=$(printf '\140') sed \ -e "s/^§1 /${hash} /" \ -e "s/^§2 /${hash}${hash} /" \ -e "s/^§3 /${hash}${hash}${hash} /" \ -e "s/```/${tick}${tick}${tick}/g" \ -e "s/`/${tick}/g" \ face_review_endpoints_summary.md \ > face_review_endpoints_standard.md ``` Nota: è importante sostituire prima ``` e poi il singolo `, come mostrato nel comando.