# People and faces

<Callout type="note" title="Beta">
The People endpoints are marked `beta`: they may change on 30 days notice rather
than the usual 90. See [versioning](/versioning).
</Callout>

Face recognition here is deliberately not automatic end to end. Detection is automatic,
but *identity* is a decision a person makes, and the API is shaped around that:

```
detection  →  suggestion  →  a human confirms  →  the person's index improves
```

The middle step is the important one. A suggestion is the matcher proposing a probable
match. It is not an assignment, and nothing is attributed to anyone until it is
confirmed.

## Detections

Faces are found during [analysis](/ai-enrichment). `GET /files/{id}/faces` lists
what was found in one photo, most confident first, each with the person it was
matched to if any:

```json
{
  "data": [
    {
      "fd_id": "3f2a9c1e-…",
      "fd_file": "8a1b6e5c-…",
      "fd_person": "7b444c0d-…",
      "fd_low_quality": false,
      "fd_box": { "x": 0.31, "y": 0.12, "width": 0.09, "height": 0.14 }
    }
  ]
}
```

`fd_low_quality` marks a face too blurry to match reliably. These detections still exist
and still display, but they are excluded from matching and from face counts, which is why
a photo with six blurry people in the background does not report six faces.

The photo's `file_metadata.faceCount` is the countable total, and `has_faces=true` on the
[media library](/organizing-assets) filters on it.

## People

A person is a cluster of faces, with or without a name attached. A face detected in
twelve photos that nobody has named is still a person, just an unnamed one.

```
GET /people
```

Most-photographed first, each with counts:

```json
{
  "data": [
    {
      "fp_id": "7b444c0d-…",
      "fp_name": "Dana Reyes",
      "face_count": 47,
      "manual_count": 6,
      "suggestion_count": 3
    }
  ]
}
```

| | |
|---|---|
| `face_count` | Faces assigned to this person. |
| `manual_count` | How many of those a human confirmed. These are the references the matcher trusts most. |
| `suggestion_count` | Faces waiting for review. |

Two filters exist for the two queues worth working through: `unnamed=true` for clusters
nobody has named, and `needs_review=true` for people with suggestions pending.

`GET /people/{id}/photos` lists the photos someone appears in and applies each file's own
access rules, so two callers asking about the same person can legitimately get different
photos back.

## Assign a face to a person

`POST /people/assign` assigns one detection. Omit `fp_id` and a new person is created for
it, which is how someone is named for the first time:

<CodeTabs syncKey="lang">

```bash title="cURL"
# name a new person from a detected face
curl -X POST https://api.genuineai.app/api/v1/people/assign \
  -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{"fd_id": "3f2a9c1e-…", "fp_name": "Dana Reyes"}'

# add a face to someone who already exists
curl -X POST https://api.genuineai.app/api/v1/people/assign \
  -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{"fd_id": "8a1b6e5c-…", "fp_id": "7b444c0d-…"}'
```

```js title="JavaScript"
await fetch("https://api.genuineai.app/api/v1/people/assign", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ fd_id: detectionId, fp_name: "Dana Reyes" }),
});
```

```python title="Python"
requests.post(
    "https://api.genuineai.app/api/v1/people/assign",
    headers=headers,
    json={"fd_id": detection_id, "fp_name": "Dana Reyes"},
)
```

</CodeTabs>

`POST /people/unassign` reverses it. `PUT /people/{id}` renames,
`POST /people/{id}/set-cover` chooses the face shown as their thumbnail, and
`DELETE /people/{id}` removes the person while leaving the underlying detections
alone.

Each confirmed assignment improves the next match. Manual assignments are the references
the matcher ranks first, so naming a handful of clear photos of someone early is worth
more than correcting fifty later.

## The review queue

```
GET /people/suggestions
```

Faces the matcher believes belong to someone already named, closest first. Filter to
one person with `fp_id`. Resolve them one at a time with
`POST /people/suggestions/{id}/confirm` or `/reject`, or in bulk:

```bash
curl -X POST https://api.genuineai.app/api/v1/people/suggestions/actions/resolve \
  -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{"action": "confirm", "fd_ids": ["3f2a9c1e-…", "8a1b6e5c-…"]}'
```

```json
{ "confirmed": 12, "rejected": 0, "skipped": 2 }
```

`skipped` counts suggestions already assigned or already resolved by someone else. The
batch does not fail over them, which matters when two reviewers work the same queue. Read
the counts to see what your call actually did.

### What the queue does not show you

The suggestion list is filtered before you see it rather than after. Three checks withhold
a suggestion rather than making you reject it:

- **Gender.** A candidate with a confidently estimated gender that never appears
  among the person's confidently-gendered references is never suggested.
- **Age.** A candidate more than 20 years from the person's median age is withheld.
- **A `no_match` verdict** from the vision check below.

This is why the queue is usually shorter than the raw match count, and why an obviously
wrong suggestion is worth reporting: it means one of these checks is not working.

### Verify suggestions with vision

```
POST /people/{id}/suggestions/verify
```

Runs one vision comparison of the person's reference faces against each unverified
candidate and records `match`, `no_match` or `unsure`. It catches what face embeddings
cannot: twins, siblings, and the same person at very different ages.

It decides nothing on its own. Verdicts come back through the suggestion list, and a
`no_match` withholds that suggestion from the queue. It is idempotent and skips
already-verified candidates, so calling it twice costs nothing. Run it before a review
session so the queue you hand to a person is already filtered.

## Merge and reindex

Two clusters representing the same person is the normal failure mode: someone
photographed across a decade, or with and without glasses.

```bash
curl -X POST https://api.genuineai.app/api/v1/people/merge \
  -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{"source_id": "3f2a9c1e-…", "target_id": "7b444c0d-…"}'
```

Every face moves from `source_id` onto `target_id`, and the source is retired. There is no
unmerge, so confirm the direction before calling it.

`POST /people/{id}/reindex` rebuilds a person's face index from their current assignments.
Run it after a large merge or a batch of corrections, so subsequent matching uses the
improved reference set.

## Search by face

Two options, answering different questions:

| | |
|---|---|
| `face_person=<fp_id>` on `GET /media-files` | Photos of someone **you have named**. Exact, cheap, no model call. |
| `face_embedding=[…]` | Photos resembling a face **you supply**, named or not. See [AI enrichment](/ai-enrichment#find-this-face). |

Use the first whenever the person already exists. It is a lookup rather than a search.

## How matching works

Every face is reduced to a numeric descriptor, and matching compares the Euclidean distance
between two of them. Three outcomes follow. Close enough, and the face is assigned
automatically. In a middle band, it becomes a suggestion for someone to confirm or reject.
Beyond that, it is not treated as a match at all.

That middle band is the entire review queue. Tightening the point where a match becomes
automatic sends more faces to review and produces fewer mistakes; loosening it does the
reverse. Faces below a minimum size are never matched automatically whatever the distance,
because a small crop's descriptor is not reliable.

The thresholds are workspace-level tuning rather than per-request parameters, and the
defaults are calibrated against the canonical cutoff for the descriptor in use.

## Next steps

- [AI enrichment](/ai-enrichment) covers what detection runs alongside.
- [Organizing and finding assets](/organizing-assets) shows the face filters in context.
- [Sharing and access](/sharing-and-access) explains why two people see different photos of the same person.
