703 lines
14 KiB
Markdown
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.
|