galleria_photo_face_rknn_op.../face_review_endpoints.md
2026-10-03 12:16:58 +02:00

703 lines
14 KiB
Markdown

# 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 <token>
```
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 <token>
```
Richiesta Admin:
```http
GET /face-review/context?user=Fabio
Authorization: Bearer <admin-token>
```
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=<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/<cluster-id>
```
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/<person-id>
```
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/<encoded-face-id>/thumbnail
GET /face-review/faces/<encoded-face-id>/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 <token>
```
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/<person-id>
Content-Type: application/json
Authorization: Bearer <token>
```
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/<person-id>/faces/<encoded-face-id>
Authorization: Bearer <token>
```
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/<cluster-id>/assign-person
Content-Type: application/json
Authorization: Bearer <token>
```
Body:
```json
{
"person_id": "<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/<encoded-face-id>/assign-person
Content-Type: application/json
Authorization: Bearer <token>
```
Body:
```json
{
"person_id": "<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": "<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.