fix: stranger_id=NULL on bind/merge; doc: add traces+mergeinto endpoints

This commit is contained in:
Accusys
2026-05-25 03:02:19 +08:00
parent 78923a8973
commit 8fdd1d741b
3 changed files with 279 additions and 5 deletions

View File

@@ -234,6 +234,11 @@ Bind a face detection to an identity. Associates the face trace with the identit
| `file_uuid` | string | Yes | File where face is detected |
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
#### Side Effects
- 清除該 face detection row 的 `stranger_id`(設為 NULL
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
#### Example
```bash
@@ -259,6 +264,11 @@ Bind all face detections of a trace to an identity. Updates all rows in `face_de
| `file_uuid` | string | Yes | File where trace exists |
| `trace_id` | integer | Yes | Trace ID (from `face_detections.trace_id`) |
#### Side Effects
- 清除該 trace 所有 face detection rows 的 `stranger_id`(設為 NULL
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
#### Example
```bash
@@ -287,6 +297,78 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
---
### `GET /api/v1/identity/:identity_uuid/traces`
**Auth**: Required
**Scope**: identity-level
Get paginated face traces (continuous tracking segments) associated with this identity across all files.
#### Query Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | integer | No | `1` | Page number |
| `page_size` | integer | No | `20` | Items per page |
#### Example
```bash
curl -s "$API/api/v1/identity/$IDENTITY_UUID/traces?page=1&page_size=3" \
-H "X-API-Key: $KEY" | jq '{total, total_faces, traces}'
```
#### Response (200)
```json
{
"success": true,
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
"name": "Cary Grant",
"total": 18,
"page": 1,
"page_size": 3,
"total_faces": 542,
"traces": [
{
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
"trace_id": 906,
"frame_count": 52,
"first_frame": 37974,
"last_frame": 38127,
"first_sec": 1519.0,
"last_sec": 1525.1,
"avg_confidence": 0.8254
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Always `true` |
| `identity_uuid` | string | Identity UUID |
| `name` | string | Identity display name |
| `total` | integer | Total number of traces (across all pages) |
| `total_faces` | integer | Sum of all face detections in returned traces |
| `traces[].file_uuid` | string | File where trace exists |
| `traces[].trace_id` | integer | Trace tracking ID |
| `traces[].frame_count` | integer | Number of frames in this trace |
| `traces[].first_frame` | integer | Start frame number |
| `traces[].last_frame` | integer | End frame number |
| `traces[].first_sec` | float | Start time in seconds |
| `traces[].last_sec` | float | End time in seconds |
| `traces[].avg_confidence` | float | Average detection confidence (0.01.0) |
#### Error Responses
| HTTP | When |
|------|------|
| `404` | Identity not found |
| `500` | Database error |
---
### `POST /api/v1/identity/:identity_uuid/unbind`
**Auth**: Required
@@ -294,6 +376,61 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
Unbind a face detection from an identity. Removes the identity association from the face record.
#### Side Effects
- 只清除 `identity_id`(設為 NULL**不會恢復 `stranger_id`**
- 被 unbind 的 face 不會自動成為 stranger
- 要重新標記為 stranger 需重新跑 Agent API`identity/analyze`
---
### `POST /api/v1/identity/:identity_uuid/mergeinto`
**Auth**: Required
**Scope**: identity-level
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
#### Request Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `into_uuid` | string | Yes | — | Target identity UUID to merge into |
| `keep_history` | bool | No | `true` | Keep source identity record with `status='merged'` (`true`) or delete it (`false`) |
#### Side Effects
- 轉移所有 `face_detections.identity_id` 到目標 identity
- 同時清除所有被轉移 rows 的 `stranger_id`
- `keep_history: true`預設source identity 設為 `status='merged'`,保留記錄
- `keep_history: false`**刪除** source identity 及其 identity JSON 檔案
#### Example
```bash
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
```
#### Response (200)
```json
{
"success": true,
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
"data": { "faces_transferred": 52 }
}
```
#### Error Responses
| HTTP | When |
|------|------|
| `404` | Source or target identity not found |
| `500` | Database error |
---
### `GET /api/v1/identities/search`
@@ -491,4 +628,4 @@ PATCH /api/v1/identity/:identity_uuid
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
---
*Updated: 2026-05-22
*Updated: 2026-05-25

View File

@@ -234,6 +234,11 @@ Bind a face detection to an identity. Associates the face trace with the identit
| `file_uuid` | string | Yes | File where face is detected |
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
#### Side Effects
- 清除該 face detection row 的 `stranger_id`(設為 NULL
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
#### Example
```bash
@@ -259,6 +264,11 @@ Bind all face detections of a trace to an identity. Updates all rows in `face_de
| `file_uuid` | string | Yes | File where trace exists |
| `trace_id` | integer | Yes | Trace ID (from `face_detections.trace_id`) |
#### Side Effects
- 清除該 trace 所有 face detection rows 的 `stranger_id`(設為 NULL
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
#### Example
```bash
@@ -287,6 +297,78 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
---
### `GET /api/v1/identity/:identity_uuid/traces`
**Auth**: Required
**Scope**: identity-level
Get paginated face traces (continuous tracking segments) associated with this identity across all files.
#### Query Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | integer | No | `1` | Page number |
| `page_size` | integer | No | `20` | Items per page |
#### Example
```bash
curl -s "$API/api/v1/identity/$IDENTITY_UUID/traces?page=1&page_size=3" \
-H "X-API-Key: $KEY" | jq '{total, total_faces, traces}'
```
#### Response (200)
```json
{
"success": true,
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
"name": "Cary Grant",
"total": 18,
"page": 1,
"page_size": 3,
"total_faces": 542,
"traces": [
{
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
"trace_id": 906,
"frame_count": 52,
"first_frame": 37974,
"last_frame": 38127,
"first_sec": 1519.0,
"last_sec": 1525.1,
"avg_confidence": 0.8254
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Always `true` |
| `identity_uuid` | string | Identity UUID |
| `name` | string | Identity display name |
| `total` | integer | Total number of traces (across all pages) |
| `total_faces` | integer | Sum of all face detections in returned traces |
| `traces[].file_uuid` | string | File where trace exists |
| `traces[].trace_id` | integer | Trace tracking ID |
| `traces[].frame_count` | integer | Number of frames in this trace |
| `traces[].first_frame` | integer | Start frame number |
| `traces[].last_frame` | integer | End frame number |
| `traces[].first_sec` | float | Start time in seconds |
| `traces[].last_sec` | float | End time in seconds |
| `traces[].avg_confidence` | float | Average detection confidence (0.01.0) |
#### Error Responses
| HTTP | When |
|------|------|
| `404` | Identity not found |
| `500` | Database error |
---
### `POST /api/v1/identity/:identity_uuid/unbind`
**Auth**: Required
@@ -294,6 +376,61 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
Unbind a face detection from an identity. Removes the identity association from the face record.
#### Side Effects
- 只清除 `identity_id`(設為 NULL**不會恢復 `stranger_id`**
- 被 unbind 的 face 不會自動成為 stranger
- 要重新標記為 stranger 需重新跑 Agent API`identity/analyze`
---
### `POST /api/v1/identity/:identity_uuid/mergeinto`
**Auth**: Required
**Scope**: identity-level
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
#### Request Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `into_uuid` | string | Yes | — | Target identity UUID to merge into |
| `keep_history` | bool | No | `true` | Keep source identity record with `status='merged'` (`true`) or delete it (`false`) |
#### Side Effects
- 轉移所有 `face_detections.identity_id` 到目標 identity
- 同時清除所有被轉移 rows 的 `stranger_id`
- `keep_history: true`預設source identity 設為 `status='merged'`,保留記錄
- `keep_history: false`**刪除** source identity 及其 identity JSON 檔案
#### Example
```bash
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
```
#### Response (200)
```json
{
"success": true,
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
"data": { "faces_transferred": 52 }
}
```
#### Error Responses
| HTTP | When |
|------|------|
| `404` | Source or target identity not found |
| `500` | Database error |
---
### `GET /api/v1/identities/search`
@@ -491,4 +628,4 @@ PATCH /api/v1/identity/:identity_uuid
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
---
*Updated: 2026-05-22
*Updated: 2026-05-25

View File

@@ -100,7 +100,7 @@ pub async fn bind_identity(
// Direct UPDATE face_detections.identity_id
let result = sqlx::query(&format!(
"UPDATE {} SET identity_id = $1 WHERE file_uuid = $2 AND face_id = $3",
"UPDATE {} SET identity_id = $1, stranger_id = NULL WHERE file_uuid = $2 AND face_id = $3",
table
))
.bind(identity_id)
@@ -272,7 +272,7 @@ pub async fn merge_identities(
// Transfer all face bindings from source → target
let updated = sqlx::query(&format!(
"UPDATE {} SET identity_id = $1 WHERE identity_id = $2",
"UPDATE {} SET identity_id = $1, stranger_id = NULL WHERE identity_id = $2",
face_table
))
.bind(into_id)
@@ -427,7 +427,7 @@ pub async fn bind_identity_trace(
})?;
let result = sqlx::query(&format!(
"UPDATE {} SET identity_id = $1 WHERE file_uuid = $2 AND trace_id = $3",
"UPDATE {} SET identity_id = $1, stranger_id = NULL WHERE file_uuid = $2 AND trace_id = $3",
fd_table
))
.bind(identity_id)