feat: progressive multi-round face matching + pending person API

- Identity agent: per-face max matching, multi-round with derived
  seeds from high-confidence faces, angle diversity filter (cosine sim < 0.90)
- Pending person API: POST /file/:file_uuid/pending-person
  + GET /file/:file_uuid/pending-persons with status=pending, source=manual
- Update API docs (07_identity.md)
This commit is contained in:
Accusys
2026-06-24 03:42:04 +08:00
parent 766a1d9a6d
commit 14e886cc08
31 changed files with 5882 additions and 742 deletions

View File

@@ -923,6 +923,128 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID/json" \
---
---
### `POST /api/v1/file/:file_uuid/pending-person`
**Auth**: Required
**Scope**: file-level
Create a manually managed "pending person" under a specific file. A pending person is an identity with `status='pending'` and `source='manual'`, used for unmatched traces that the user wants to manually label before a full identity resolution.
Optionally binds a list of trace IDs to this new identity.
#### Request
```json
{
"trace_ids": [100, 150, 200],
"name": "Mystery Man #1"
}
```
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `trace_ids` | array[int] | No | `[]` | Trace IDs to bind to this pending person |
| `name` | string | No | `"Person N"` | Human-readable name. Auto-generated if omitted |
#### Example
```bash
# Create pending person with name and no traces
curl -s -X POST "$API/api/v1/file/$FILE_UUID/pending-person" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Unknown Woman #2", "trace_ids": []}'
# Create pending person with auto-name and bind traces
curl -s -X POST "$API/api/v1/file/$FILE_UUID/pending-person" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"trace_ids": [100, 150, 200]}'
```
#### Response (200)
```json
{
"success": true,
"message": "Created pending person: Mystery Man #1 (uuid: 4d96b25b-68f0-4c52-b238-d69f7dfd588b)",
"data": {
"identity_uuid": "4d96b25b-68f0-4c52-b238-d69f7dfd588b",
"identity_id": 55,
"name": "Mystery Man #1",
"bound_traces": 0
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `identity_uuid` | string | UUID of the newly created pending identity |
| `identity_id` | integer | Internal ID of the new identity |
| `name` | string | Display name |
| `bound_traces` | integer | Number of traces bound |
#### Side Effects
- Creates an `identities` row with `status='pending'`, `source='manual'`, `file_uuid=<file_uuid>`
- If `trace_ids` provided: `UPDATE face_detections SET identity_id = ...` for matching traces
- If `trace_ids` provided: TKG face_track nodes get `identity_id` / `identity_name` in properties
- Identity JSON file synced to disk
---
### `GET /api/v1/file/:file_uuid/pending-persons`
**Auth**: Required
**Scope**: file-level
List all pending persons for a file.
#### Example
```bash
curl -s "$API/api/v1/file/$FILE_UUID/pending-persons" \
-H "X-API-Key: $KEY"
```
#### Response (200)
```json
{
"success": true,
"message": "Found 2 pending persons for c36f35685177c981aa139b66bbbccc5b",
"data": [
{
"identity_uuid": "232ecd08-a2bf-4bd0-bd25-0bd8fb7a7dae",
"identity_id": 56,
"name": "Person 2",
"created_at": "2026-06-23 17:13:23",
"trace_count": 3,
"bound_traces": null
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `identity_uuid` | string | Identity UUID |
| `identity_id` | integer | Internal identity ID |
| `name` | string | Display name |
| `created_at` | string | Creation timestamp |
| `trace_count` | integer | Number of face traces bound to this pending person |
| `bound_traces` | array[int] | List of bound trace IDs (currently null, reserved for future expansion) |
#### Notes
- Pending persons are normal `identities` rows with `status='pending'` — they can be promoted to confirmed via `PATCH /api/v1/identity/:identity_uuid` (`{"status": "confirmed"}`)
- They can be merged into known identities via `POST /api/v1/identity/:identity_uuid/mergeinto`
- Use `GET /api/v1/identity/:identity_uuid/traces` to get detailed trace info for each pending person
---
## Alias System (BCP 47 Locale Tags)
Identity aliases support multilingual display names. Aliases are stored in `metadata.aliases` as an array of `{locale, name}` objects.

View File

@@ -32,7 +32,7 @@ a { color: #0066cc; }
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: lookup -->
<!-- description: File lookup by name and unregistration -->
<!-- description: File listing, lookup by name, file detail, faces, identities, JSON download, unregistration -->
<!-- depends: 01_auth, 03_register -->
<h2>File Lookup</h2>
@@ -137,6 +137,537 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</s
</tbody>
</table>
<hr />
<hr />
<h2>File Listing</h2>
<h3><code>GET /api/v1/files</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>List all registered files with pagination. Optionally filter by status or fetch a specific file by UUID.</p>
<h4>Query Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td>1</td>
<td>Page number</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>No</td>
<td>20</td>
<td>Items per page</td>
</tr>
<tr>
<td><code>status</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by status: <code>registered</code>, <code>processing</code>, <code>completed</code>, <code>failed</code>, <code>indexed</code>, <code>checked_out</code></td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Fetch a specific file (returns as single-item list)</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code><span class="c1"># List all files (paginated)</span>
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/files?page=1&amp;page_size=10&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
<span class="c1"># Filter by status</span>
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/files?status=completed&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
<span class="c1"># Fetch specific file</span>
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/files?file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;video.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_path&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/path/to/video.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;completed&quot;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total file count</td>
</tr>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>Current page</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>Items per page</td>
</tr>
<tr>
<td><code>data</code></td>
<td>array</td>
<td>Array of file items</td>
</tr>
<tr>
<td><code>data[].file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>data[].file_name</code></td>
<td>string</td>
<td>Registered file name</td>
</tr>
<tr>
<td><code>data[].file_path</code></td>
<td>string</td>
<td>Full filesystem path</td>
</tr>
<tr>
<td><code>data[].status</code></td>
<td>string</td>
<td>Processing status</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get detailed info for a specific registered file including metadata, duration, FPS, and probe data.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;video.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_path&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/path/to/video.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;completed&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;duration&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">120.5</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;fps&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;metadata&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;format&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">&quot;duration&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;120.5&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;size&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;794863677&quot;</span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;streams&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nt">&quot;codec_name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;h264&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;width&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;height&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">}]</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-05-16T12:00:00Z&quot;</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>file_name</code></td>
<td>string</td>
<td>Registered file name</td>
</tr>
<tr>
<td><code>file_path</code></td>
<td>string</td>
<td>Full filesystem path</td>
</tr>
<tr>
<td><code>status</code></td>
<td>string</td>
<td>Processing status</td>
</tr>
<tr>
<td><code>duration</code></td>
<td>float</td>
<td>Duration in seconds</td>
</tr>
<tr>
<td><code>fps</code></td>
<td>float</td>
<td>Frames per second</td>
</tr>
<tr>
<td><code>metadata</code></td>
<td>object</td>
<td>Full ffprobe metadata (probe.json)</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>string</td>
<td>Registration timestamp (ISO 8601)</td>
</tr>
</tbody>
</table>
<h4>Error Codes</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>File UUID not found</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/identities</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get all identities present in a specific file with pagination.</p>
<h4>Query Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td>1</td>
<td>Page number</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>No</td>
<td>20</td>
<td>Items per page</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/identities?page=1&amp;page_size=50&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;fps&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;identity_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a90105-6d6b-46ff-92da-0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Audrey Hepburn&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;metadata&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">&quot;source&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;tmdb&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;tmdb_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1234</span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;face_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">142</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;speaker_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;start_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;end_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5000</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;start_time&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">4.17</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;end_time&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">208.33</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.87</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>data[].identity_id</code></td>
<td>integer</td>
<td>Database identity ID</td>
</tr>
<tr>
<td><code>data[].identity_uuid</code></td>
<td>string/null</td>
<td>Global identity UUID (null if unbound)</td>
</tr>
<tr>
<td><code>data[].name</code></td>
<td>string</td>
<td>Identity name</td>
</tr>
<tr>
<td><code>data[].metadata</code></td>
<td>object</td>
<td>Source metadata (TMDb, etc.)</td>
</tr>
<tr>
<td><code>data[].face_count</code></td>
<td>integer/null</td>
<td>Number of face detections</td>
</tr>
<tr>
<td><code>data[].speaker_count</code></td>
<td>integer/null</td>
<td>Number of speaker segments</td>
</tr>
<tr>
<td><code>data[].start_frame</code></td>
<td>integer/null</td>
<td>First appearance frame</td>
</tr>
<tr>
<td><code>data[].end_frame</code></td>
<td>integer/null</td>
<td>Last appearance frame</td>
</tr>
<tr>
<td><code>data[].start_time</code></td>
<td>float/null</td>
<td>First appearance time (seconds)</td>
</tr>
<tr>
<td><code>data[].end_time</code></td>
<td>float/null</td>
<td>Last appearance time (seconds)</td>
</tr>
<tr>
<td><code>data[].confidence</code></td>
<td>float/null</td>
<td>Average detection confidence</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/faces</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>List all face detections in a specific file with pagination.</p>
<h4>Query Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td>1</td>
<td>Page number</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>No</td>
<td>50</td>
<td>Items per page</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/faces?page=1&amp;page_size=100&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1420</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">50</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face_100&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_number&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;timestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">50.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;bbox&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="p">,</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span><span class="w"> </span><span class="mi">400</span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.95</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a90105-6d6b-46ff-92da-0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>data[].face_id</code></td>
<td>string</td>
<td>Face detection ID</td>
</tr>
<tr>
<td><code>data[].frame_number</code></td>
<td>integer</td>
<td>Frame number in video</td>
</tr>
<tr>
<td><code>data[].timestamp</code></td>
<td>float</td>
<td>Timestamp in seconds</td>
</tr>
<tr>
<td><code>data[].bbox</code></td>
<td>array</td>
<td>Bounding box <code>[x1, y1, x2, y2]</code></td>
</tr>
<tr>
<td><code>data[].confidence</code></td>
<td>float</td>
<td>Detection confidence</td>
</tr>
<tr>
<td><code>data[].identity_id</code></td>
<td>integer/null</td>
<td>Bound identity ID (null if unbound)</td>
</tr>
<tr>
<td><code>data[].identity_uuid</code></td>
<td>string/null</td>
<td>Bound identity UUID (null if unbound)</td>
</tr>
<tr>
<td><code>data[].trace_id</code></td>
<td>integer/null</td>
<td>Face trace ID (null if not traced)</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/json/:processor</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Download raw JSON output for a specific processor.</p>
<h4>Path Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>processor</code></td>
<td>string</td>
<td>Yes</td>
<td>Processor name: <code>cut</code>, <code>asrx</code>, <code>yolo</code>, <code>ocr</code>, <code>face</code>, <code>pose</code>, <code>story</code>, etc.</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/json/face&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">&#39;.frames | length&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<p>Returns the raw JSON output of the specified processor. Structure varies by processor type.</p>
<h4>Error Codes</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>JSON file not found</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Failed to parse JSON</td>
</tr>
</tbody>
</table>
<hr />
<h2>Unregister</h2>
<h3><code>POST /api/v1/unregister</code></h3>
<p><strong>Auth</strong>: Required
@@ -293,7 +824,7 @@ curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>
</tbody>
</table>
<hr />
<p><em>Updated: 2026-05-19 12:49:24</em></p>
<p><em>Updated: 2026-06-20 — Added file listing, file detail, file identities, file faces, and JSON download endpoints</em></p>
</div>
</body>
</html>

View File

@@ -260,10 +260,11 @@ curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/progress/:file_uuid</code></h3>
<h3><code>POST /api/v1/progress/:file_uuid</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get real-time processing progress for a file via Redis pub/sub. Includes per-processor status, current/total frames, ETA, and system resource stats.</p>
<p><strong>Note</strong>: This endpoint uses <strong>POST</strong> method, not GET. The progress data is stored in Redis as a hash, and POST is used to retrieve the latest state.</p>
<h4>Pipeline Order</h4>
<table class="table">
<thead>
@@ -339,7 +340,7 @@ curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>
</table>
<p>All processors except <code>story</code> and <code>5w1h</code> run concurrently when their dependencies are met. Story and 5W1H run sequentially after their prerequisites.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">&#39;{overall_progress, processors: [.processors[] | {processor_type, status}]}&#39;</span>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">&#39;{overall_progress, processors: [.processors[] | {name, status}]}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
@@ -506,8 +507,152 @@ curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>
</tr>
</tbody>
</table>
<h3><code>GET /api/v1/file/:file_uuid/processor-counts</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get counts of processor JSON output files. See <code>15_tkg.md</code> for full documentation.</p>
<hr />
<p><em>Updated: 2026-05-19 12:49:24</em></p>
<h2>Pipeline Steps (Manual)</h2>
<p>These endpoints execute individual pipeline steps. They are typically called by the worker automatically, but can be invoked manually for debugging or re-processing.</p>
<h3><code>POST /api/v1/file/:file_uuid/store-asrx</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Store ASRX diarization results as chunk records in the database. Converts ASRX segments into searchable chunk entries.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/store-asrx&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;ASRX chunks stored&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;3a6c1865...&quot;</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/rule1</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Execute Rule 1 pipeline step. Applies rule-based chunking to create structured chunk records from processor outputs.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/rule1&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Rule 1 complete: 45 chunks&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;3a6c1865...&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunks&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>message</code></td>
<td>string</td>
<td>Human-readable completion message</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>chunks</code></td>
<td>integer</td>
<td>Number of chunks produced</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/vectorize</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Generate vector embeddings for all chunks of a file and store them in Qdrant for semantic search.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/vectorize&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Vectorization complete&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;3a6c1865...&quot;</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/phase1</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Execute Phase 1 of the post-processing pipeline. Combines store-asrx, rule1, and vectorize into a single step.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/phase1&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Phase 1 complete&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;3a6c1865...&quot;</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/complete</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Mark a video as fully processed. Updates the video status to <code>completed</code> and finalizes all pipeline state.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/complete&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Video marked as completed&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;3a6c1865...&quot;</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3>Pipeline Step Order</h3>
<div class="codehilite"><pre><span></span><code> process (trigger)
├─→ cut, yolo, ocr, face, pose, asrx (parallel processors)
├─→ store-asrx (store diarization as chunks)
├─→ rule1 (rule-based chunking)
├─→ vectorize (embed chunks to Qdrant)
└─→ complete (mark done)
</code></pre></div>
<p>Phase 1 (<code>/phase1</code>) combines store-asrx + rule1 + vectorize into one call.</p>
<hr />
<p><em>Updated: 2026-06-20 12:00:00</em></p>
</div>
</body>
</html>

View File

@@ -32,7 +32,7 @@ a { color: #0066cc; }
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: search -->
<!-- description: Vector search, BM25, smart search, universal search, visual search -->
<!-- description: Vector search, BM25, smart search, universal search, LLM reranked search, frame search -->
<!-- depends: 01_auth -->
<h2>Search APIs</h2>
@@ -282,9 +282,251 @@ a { color: #0066cc; }
<h3><code>POST /api/v1/search/frames</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: global / file-level</p>
<p>Search face detection frames by identity name or trace ID.</p>
<p>Search frames by YOLO objects, OCR text, face IDs, or pose detections. Filters frames based on visual content detected during processing.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Restrict to specific file</td>
</tr>
<tr>
<td><code>object_class</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by YOLO object class (e.g., <code>person</code>, <code>car</code>, <code>dog</code>)</td>
</tr>
<tr>
<td><code>ocr_text</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by OCR text content (ILIKE match)</td>
</tr>
<tr>
<td><code>face_id</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by face detection ID</td>
</tr>
<tr>
<td><code>time_range</code></td>
<td>[float, float]</td>
<td>No</td>
<td></td>
<td>Filter by time range <code>[start_secs, end_secs]</code></td>
</tr>
<tr>
<td><code>limit</code></td>
<td>integer</td>
<td>No</td>
<td>100</td>
<td>Max results</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code><span class="c1"># Search for frames containing &quot;person&quot; objects</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/search/frames&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;file_uuid&quot;: &quot;&#39;</span><span class="s2">&quot;</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="s1">&#39;&quot;, &quot;object_class&quot;: &quot;person&quot;, &quot;limit&quot;: 20}&#39;</span>
<span class="c1"># Search for frames with specific OCR text</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/search/frames&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;file_uuid&quot;: &quot;&#39;</span><span class="s2">&quot;</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="s1">&#39;&quot;, &quot;ocr_text&quot;: &quot;hello&quot;, &quot;time_range&quot;: [10.0, 30.0]}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;frames&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;frame_number&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;timestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">50.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;objects&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nt">&quot;class&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;person&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.95</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;bbox&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="p">,</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span><span class="w"> </span><span class="mi">400</span><span class="p">]}],</span>
<span class="w"> </span><span class="nt">&quot;ocr_texts&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;Hello World&quot;</span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;faces&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face_42&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.88</span><span class="p">}],</span>
<span class="w"> </span><span class="nt">&quot;pose_persons&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;bbox&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">120</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="p">,</span><span class="w"> </span><span class="mi">280</span><span class="p">,</span><span class="w"> </span><span class="mi">380</span><span class="p">]}]</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>frames</code></td>
<td>array</td>
<td>Array of matching frame objects</td>
</tr>
<tr>
<td><code>frames[].frame_number</code></td>
<td>integer</td>
<td>Frame number in video</td>
</tr>
<tr>
<td><code>frames[].timestamp</code></td>
<td>float</td>
<td>Timestamp in seconds</td>
</tr>
<tr>
<td><code>frames[].file_uuid</code></td>
<td>string</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>frames[].objects</code></td>
<td>array/null</td>
<td>YOLO detections in this frame</td>
</tr>
<tr>
<td><code>frames[].ocr_texts</code></td>
<td>array/null</td>
<td>OCR text strings in this frame</td>
</tr>
<tr>
<td><code>frames[].faces</code></td>
<td>array/null</td>
<td>Face detections in this frame</td>
</tr>
<tr>
<td><code>frames[].pose_persons</code></td>
<td>array/null</td>
<td>Pose-detected persons in this frame</td>
</tr>
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total matching frame count</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/search/identity_text</code></h3>
<h3><code>POST /api/v1/search/llm-smart</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: global / file-level</p>
<p>Smart search with LLM re-ranking. First fetches candidate results via RRF (Reciprocal Rank Fusion) using the existing smart search, then uses an LLM (Gemma4 on port 8000) to re-rank candidates by relevance to the query.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>query</code></td>
<td>string</td>
<td>Yes</td>
<td></td>
<td>Search text</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>File UUID to search within</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>integer</td>
<td>No</td>
<td>10</td>
<td>Max results to return</td>
</tr>
</tbody>
</table>
<h4>Pipeline</h4>
<div class="codehilite"><pre><span></span><code><span class="w"> </span><span class="mf">1.</span><span class="w"> </span><span class="n">smart_search</span><span class="w"> </span><span class="n"></span><span class="w"> </span><span class="k">fetch</span><span class="w"> </span><span class="n">N</span><span class="w"> </span><span class="n">candidates</span><span class="w"> </span><span class="p">(</span><span class="k">limit</span><span class="w"> </span><span class="n">×</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="n">clamped</span><span class="w"> </span><span class="mi">10</span><span class="o">-</span><span class="mi">20</span><span class="p">)</span>
<span class="w"> </span><span class="mf">2.</span><span class="w"> </span><span class="n">LLM</span><span class="w"> </span><span class="n">rerank</span><span class="w"> </span><span class="n"></span><span class="w"> </span><span class="n">re</span><span class="o">-</span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="n">relevance</span><span class="w"> </span><span class="k">using</span><span class="w"> </span><span class="n">Gemma4</span>
<span class="w"> </span><span class="mf">3.</span><span class="w"> </span><span class="n">trim</span><span class="w"> </span><span class="n"></span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">top</span><span class="w"> </span><span class="n n-Quoted">`limit`</span><span class="w"> </span><span class="n">results</span>
</code></pre></div>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/search/llm-smart&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;query&quot;: &quot;two people having a conversation about business&quot;, &quot;limit&quot;: 5}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;query&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;two people having a conversation about business&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;results&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;parent_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1234</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;scene_order&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1234</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;start_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5000</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;end_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;fps&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;start_time&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">208.3</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;end_time&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">216.7</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;summary&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;[208s-217s, 9s] Two people discussing project timeline...&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;similarity&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.72</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;strategy&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;llm_reranked&quot;</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>strategy</code></td>
<td>string</td>
<td>Always <code>"llm_reranked"</code> for this endpoint</td>
</tr>
<tr>
<td><code>results</code></td>
<td>array</td>
<td>Re-ranked search results (same format as smart search)</td>
</tr>
</tbody>
</table>
<h4>Fallback</h4>
<p>If LLM reranking fails (model unavailable, timeout), falls back to RRF order without error.</p>
<hr />
<h3>Visual Search</h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: global / file-level</p>
<p>Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.</p>
@@ -392,12 +634,13 @@ a { color: #0066cc; }
</tbody>
</table>
<hr />
<h3>Visual Search</h3>
<h3>Visual Search (Planned)</h3>
<table class="table">
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Status</th>
<th>Description</th>
</tr>
</thead>
@@ -405,26 +648,31 @@ a { color: #0066cc; }
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual</code></td>
<td>Not implemented</td>
<td>Search visual chunks</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/class</code></td>
<td>Not implemented</td>
<td>Search by object class</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/density</code></td>
<td>Not implemented</td>
<td>Search by object density</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/combination</code></td>
<td>Not implemented</td>
<td>Search by object combination</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/stats</code></td>
<td>Not implemented</td>
<td>Visual chunk statistics</td>
</tr>
</tbody>
@@ -457,7 +705,7 @@ a { color: #0066cc; }
</tbody>
</table>
<hr />
<p><em>Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs</em></p>
<p><em>Updated: 2026-06-20 — Added llm-smart search, completed frames search documentation, marked visual search as planned</em></p>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -790,7 +790,100 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</s
</tbody>
</table>
<hr />
<p><em>Updated: 2026-05-19 12:49:24</em></p>
<h3><code>GET /api/v1/file/:file_uuid/stranger/:stranger_id/representative-face</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get the representative face for a stranger (unidentified face trace).</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/stranger/1/representative-face&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;stranger_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">85</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;representative&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;frame_number&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5000</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;timestamp_secs&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">208.33</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;bbox&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">&quot;x&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;y&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;width&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">150</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;height&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">150</span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.92</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;quality_score&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">20700</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;blur_score&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">8.5</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/stranger/:stranger_id/thumbnail</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Extract the best face image for a stranger as JPEG (320×320).</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/stranger/1/thumbnail&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span>-o<span class="w"> </span>stranger_1_face.jpg
</code></pre></div>
<h4>Response</h4>
<ul>
<li><strong>200</strong>: <code>image/jpeg</code> binary data (320×320 cropped face)</li>
<li><strong>404</strong>: File or stranger not found</li>
</ul>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/chunk/:chunk_id/thumbnail</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get thumbnail for a specific chunk. Extracts the representative frame for the chunk's time range.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/chunk/chunk_1/thumbnail&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span>-o<span class="w"> </span>chunk_1.jpg
</code></pre></div>
<h4>Response</h4>
<ul>
<li><strong>200</strong>: <code>image/jpeg</code> binary data</li>
<li><strong>404</strong>: File or chunk not found</li>
</ul>
<hr />
<h3><code>GET /api/v1/media-proxy</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Proxy request to fetch media from external URLs. Useful for loading profile images or thumbnails from external services (TMDb, etc.) without exposing the external URL to the client.</p>
<h4>Query Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>string</td>
<td>Yes</td>
<td>External URL to proxy</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/media-proxy?url=https://image.tmdb.org/t/p/w500/abc123.jpg&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span>-o<span class="w"> </span>tmdb_profile.jpg
</code></pre></div>
<h4>Response</h4>
<ul>
<li><strong>200</strong>: Proxied media data (Content-Type from external source)</li>
<li><strong>400</strong>: Missing or invalid URL parameter</li>
<li><strong>500</strong>: External request failed</li>
</ul>
<hr />
<hr />
<p><em>Updated: 2026-06-20 — Added stranger endpoints, chunk thumbnail, and media proxy</em></p>
</div>
</body>
</html>

View File

@@ -125,8 +125,124 @@ If local files exist, no external API call is made. Internet is only needed for
<span class="p">}</span>
</code></pre></div>
<h3><code>POST /api/v1/tmdb/fetch</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Fetch TMDb data by filename, create identities with profile images and embeddings. Similar to prefetch+probe combined, but also downloads profile images and generates embeddings.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>filename</code></td>
<td>string</td>
<td>Yes</td>
<td>Movie filename to search TMDb for</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/tmdb/fetch&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;filename&quot;: &quot;charade.mp4&quot;}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;movie_title&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Charade (1963)&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;tmdb_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1234</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identities_created&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;profile_images_downloaded&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<p><em>Updated: 2026-05-19 12:49:24</em></p>
<h3><code>POST /api/v1/agents/tmdb/match/:file_uuid</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Match TMDb identities to face traces using Qdrant vector similarity. Compares face embeddings against TMDb identity embeddings to find the best matches.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/agents/tmdb/match/</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;matches&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a90105-6d6b-46ff-92da-0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Audrey Hepburn&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.92</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;tmdb_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1234</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;total_matches&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>matches[].trace_id</code></td>
<td>integer</td>
<td>Face trace ID</td>
</tr>
<tr>
<td><code>matches[].identity_uuid</code></td>
<td>string</td>
<td>Matched TMDb identity UUID</td>
</tr>
<tr>
<td><code>matches[].identity_name</code></td>
<td>string</td>
<td>Identity display name</td>
</tr>
<tr>
<td><code>matches[].confidence</code></td>
<td>float</td>
<td>Cosine similarity score (0.01.0)</td>
</tr>
<tr>
<td><code>matches[].tmdb_id</code></td>
<td>integer</td>
<td>TMDb person ID</td>
</tr>
<tr>
<td><code>total_matches</code></td>
<td>integer</td>
<td>Total successful matches</td>
</tr>
</tbody>
</table>
<hr />
<h3>TMDb Auto-Match</h3>
<p>When <code>MOMENTRY_TMDB_PROBE_ENABLED=true</code>, the worker automatically runs TMDb matching during the post-process phase:</p>
<ol>
<li><strong>Register phase</strong>: Searches TMDb by filename, creates identities with <code>tmdb_id</code>/<code>tmdb_profile</code></li>
<li><strong>Post-process phase</strong>: Matches detected faces against TMDb identities via cosine similarity using Qdrant</li>
</ol>
<p>No manual API call needed if auto-match is enabled.</p>
<hr />
<p><em>Updated: 2026-06-20 — Added tmdb/fetch and tmdb/match endpoints</em></p>
</div>
</body>
</html>

View File

@@ -32,12 +32,46 @@ a { color: #0066cc; }
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: identity_history -->
<!-- description: Identity PATCH operation history, undo, and redo -->
<!-- description: Identity operation history, undo, and redo (PATCH, bind, unbind, bind_trace, mergeinto) -->
<!-- depends: 01_auth, 07_identity -->
<h2>Identity Operation History</h2>
<p>Every <code>PATCH /api/v1/identity/:identity_uuid</code> automatically records a before/after snapshot in the <code>identity_history</code> table. Use undo/redo to revert or reapply changes, and history to inspect the operation log.</p>
<h3>History System Overview</h3>
<p>Every mutation on an identity automatically records a before/after snapshot. Use undo/redo to revert or reapply changes, and history to inspect the operation log.</p>
<p>Three independent undo/redo systems exist:</p>
<table class="table">
<thead>
<tr>
<th>System</th>
<th>Storage</th>
<th>Operations Covered</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>PATCH</strong></td>
<td>PostgreSQL <code>identity_history</code></td>
<td><code>update</code></td>
</tr>
<tr>
<td><strong>Bind</strong></td>
<td>PostgreSQL <code>identity_history</code></td>
<td><code>bind</code>, <code>unbind</code>, <code>bind_trace</code></td>
</tr>
<tr>
<td><strong>Merge</strong></td>
<td>MongoDB <code>identity_merge_history</code></td>
<td>mergeinto</td>
</tr>
<tr>
<td><strong>Delete</strong></td>
<td>PostgreSQL <code>identity_history</code></td>
<td><code>delete</code></td>
</tr>
</tbody>
</table>
<hr />
<h3>1. PATCH History &amp; Undo/Redo</h3>
<h4>Overview</h4>
<table class="table">
<thead>
<tr>
@@ -64,11 +98,11 @@ a { color: #0066cc; }
</tr>
<tr>
<td>Redo stack</td>
<td>Cleared on new PATCH (<code>is_undone=true</code> records are deleted)</td>
<td>Cleared on new PATCH (<code>is_undone=true</code> + <code>operation='update'</code> records are deleted)</td>
</tr>
</tbody>
</table>
<h4>Stack Model</h4>
<h5>Stack Model</h5>
<div class="codehilite"><pre><span></span><code>PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
↓ undo
PATCH 1 → PATCH 2 (undo stack)
@@ -77,13 +111,13 @@ PATCH 1 → PATCH 2 (undo stack)
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</code></pre></div>
<p>A new PATCH after undo clears the redo stack (PATCH 3 is lost).</p>
<p>A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.</p>
<hr />
<h3><code>POST /api/v1/identity/:identity_uuid/undo</code></h3>
<h4><code>POST /api/v1/identity/:identity_uuid/undo</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Undo the most recent PATCH operations. Restores the identity's <code>before_snapshot</code> and marks the history records as undone.</p>
<h4>Request (JSON)</h4>
<h5>Request (JSON)</h5>
<table class="table">
<thead>
<tr>
@@ -104,22 +138,22 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Behavior</h4>
<h5>Behavior</h5>
<ul>
<li>Queries <code>is_undone=false</code> records, ordered by <code>created_at DESC</code></li>
<li>Queries <code>is_undone=false</code> records with <code>operation='update'</code>, ordered by <code>created_at DESC</code></li>
<li>Restores <code>name</code>, <code>identity_type</code>, <code>source</code>, <code>status</code>, <code>metadata</code>, <code>tmdb_id</code>, <code>tmdb_profile</code> from the last record's <code>before_snapshot</code></li>
<li>Marks the undone records as <code>is_undone=true</code> with <code>undone_at=NOW()</code></li>
<li>Syncs <code>identity.json</code> to disk</li>
<li>Updates <code>_index.json</code> if name changed</li>
</ul>
<h4>Example</h4>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/undo&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;steps&quot;: 1}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
@@ -159,7 +193,7 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Error Responses</h4>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
@@ -183,11 +217,11 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/identity/:identity_uuid/redo</code></h3>
<h4><code>POST /api/v1/identity/:identity_uuid/redo</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Redo previously undone PATCH operations. Restores the identity's <code>after_snapshot</code> and marks the history records as no longer undone.</p>
<h4>Request (JSON)</h4>
<h5>Request (JSON)</h5>
<table class="table">
<thead>
<tr>
@@ -208,22 +242,22 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Behavior</h4>
<h5>Behavior</h5>
<ul>
<li>Queries <code>is_undone=true</code> records, ordered by <code>created_at DESC</code></li>
<li>Queries <code>is_undone=true</code> records with <code>operation='update'</code>, ordered by <code>created_at DESC</code></li>
<li>Restores all identity fields from the last record's <code>after_snapshot</code></li>
<li>Marks records as <code>is_undone=false</code> with <code>undone_at=NULL</code></li>
<li>Syncs <code>identity.json</code> to disk</li>
<li>Updates <code>_index.json</code> if name changed</li>
</ul>
<h4>Example</h4>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/redo&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;steps&quot;: 1}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
@@ -263,7 +297,7 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Error Responses</h4>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
@@ -287,11 +321,11 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/identity/:identity_uuid/history</code></h3>
<h4><code>GET /api/v1/identity/:identity_uuid/history</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Query the operation history for an identity. Returns paginated records with undo/redo stack counts.</p>
<h4>Query Parameters</h4>
<p>Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to <code>operation='update'</code>).</p>
<h5>Query Parameters</h5>
<table class="table">
<thead>
<tr>
@@ -319,7 +353,7 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Response (200)</h4>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
@@ -357,7 +391,7 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total history records for this identity</td>
<td>Total PATCH history records for this identity</td>
</tr>
<tr>
<td><code>undo_stack_count</code></td>
@@ -396,12 +430,12 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tr>
</tbody>
</table>
<h4>Example</h4>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/history?page=1&amp;limit=10&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Error Responses</h4>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
@@ -421,45 +455,746 @@ PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
</tbody>
</table>
<hr />
<h3>Comparison: PATCH Undo vs Merge Undo</h3>
<h3>2. Bind/Unbind/Trace History &amp; Undo/Redo</h3>
<p>All three operations (<code>bind</code>, <code>unbind</code>, <code>bind_trace</code>) share a single history table and undo/redo stack.</p>
<h4>Bind Operation Overview</h4>
<table class="table">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Storage</td>
<td>PostgreSQL <code>identity_history</code> table (same table as PATCH)</td>
</tr>
<tr>
<td>Snapshot</td>
<td><code>{"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}</code></td>
</tr>
<tr>
<td>Max records</td>
<td>256 per identity (shared limit across all operation types)</td>
</tr>
<tr>
<td>Undo steps</td>
<td>Unlimited (<code>steps</code> param)</td>
</tr>
<tr>
<td>Redo stack</td>
<td>Cleared on new bind/unbind/bind_trace (<code>operation IN ('bind','unbind','bind_trace')</code> + <code>is_undone=true</code> records deleted)</td>
</tr>
<tr>
<td>Stack isolation</td>
<td>Bind redo stack is <strong>independent</strong> from PATCH redo stack — clearing one does not affect the other</td>
</tr>
</tbody>
</table>
<h5>Stack Model</h5>
<div class="codehilite"><pre><span></span><code>bind face_1 (to id=9) → unbind face_1 → bind trace 906 (to id=9)
(undo stack, is_undone=false) (undo stack) (undo stack)
↓ undo (first undone: bind_trace)
bind trace 906 (is_undone=true)
(redo stack)
↓ redo
bind face_1 → unbind face_1 → bind trace 906
(undo stack)
</code></pre></div>
<p>A new bind/unbind/trace after undo clears only the bind redo stack (operations with <code>IN ('bind','unbind','bind_trace')</code>).</p>
<h5>Snapshot Format</h5>
<p><strong>Before (bind):</strong></p>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_id_before&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="p">}</span>
</code></pre></div>
<p><strong>After (bind):</strong></p>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_id_after&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span>
<span class="p">}</span>
</code></pre></div>
<p><strong>Before (unbind) — binding existed before:</strong></p>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_id_before&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span>
<span class="p">}</span>
</code></pre></div>
<p><strong>After (unbind):</strong></p>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_id_after&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="p">}</span>
</code></pre></div>
<p>For <code>bind_trace</code>, the snapshot uses <code>trace_id</code> instead of <code>face_id</code>, with <code>identity_id_before</code> capturing the first face's identity in that trace.</p>
<hr />
<h4><code>POST /api/v1/identity/:identity_uuid/bind/undo</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Undo the most recent bind/unbind/bind_trace operations. Restores <code>identity_id_before</code> from the snapshot and marks records as undone.</p>
<h5>Request (JSON)</h5>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>steps</code></td>
<td>integer</td>
<td>No</td>
<td><code>1</code></td>
<td>Number of undo steps to apply</td>
</tr>
</tbody>
</table>
<h5>Behavior</h5>
<ul>
<li>Queries <code>is_undone=false</code> records with <code>operation IN ('bind','unbind','bind_trace')</code>, ordered by <code>created_at DESC</code></li>
<li>Restores <code>identity_id_before</code> — for bind this is <code>null</code> (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity</li>
<li>Marks the undone records as <code>is_undone=true</code> with <code>undone_at=NOW()</code></li>
</ul>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/bind/undo&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;steps&quot;: 1}&#39;</span>
</code></pre></div>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;operation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;bind&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;undone_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;affected_rows&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">53</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>operation</code></td>
<td>string</td>
<td>The actual operation undone (<code>bind</code>, <code>unbind</code>, or <code>bind_trace</code>)</td>
</tr>
<tr>
<td><code>undone_count</code></td>
<td>integer</td>
<td>Number of history records undone</td>
</tr>
<tr>
<td><code>affected_rows</code></td>
<td>integer</td>
<td>Number of <code>face_detections</code> rows updated</td>
</tr>
</tbody>
</table>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>400</code></td>
<td>No bind undo operations available</td>
</tr>
<tr>
<td><code>404</code></td>
<td>Identity not found</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Database error</td>
</tr>
</tbody>
</table>
<hr />
<h4><code>POST /api/v1/identity/:identity_uuid/bind/redo</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Redo previously undone bind/unbind/bind_trace operations. Restores <code>identity_id_after</code> from the snapshot.</p>
<h5>Request (JSON)</h5>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>steps</code></td>
<td>integer</td>
<td>No</td>
<td><code>1</code></td>
<td>Number of redo steps to apply</td>
</tr>
</tbody>
</table>
<h5>Behavior</h5>
<ul>
<li>Queries <code>is_undone=true</code> records with <code>operation IN ('bind','unbind','bind_trace')</code>, ordered by <code>created_at DESC</code></li>
<li>Restores <code>identity_id_after</code> — for bind this is the identity the face was bound to, for unbind this is <code>null</code></li>
<li>Marks records as <code>is_undone=false</code> with <code>undone_at=NULL</code></li>
</ul>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/bind/redo&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;steps&quot;: 1}&#39;</span>
</code></pre></div>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;operation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;unbind&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;redone_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;affected_rows&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>operation</code></td>
<td>string</td>
<td>The actual operation redone (<code>bind</code>, <code>unbind</code>, or <code>bind_trace</code>)</td>
</tr>
<tr>
<td><code>redone_count</code></td>
<td>integer</td>
<td>Number of history records redone</td>
</tr>
<tr>
<td><code>affected_rows</code></td>
<td>integer</td>
<td>Number of <code>face_detections</code> rows updated</td>
</tr>
</tbody>
</table>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>400</code></td>
<td>No bind redo operations available</td>
</tr>
<tr>
<td><code>404</code></td>
<td>Identity not found</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Database error</td>
</tr>
</tbody>
</table>
<hr />
<h4><code>GET /api/v1/identity/:identity_uuid/bind/history</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.</p>
<h5>Query Parameters</h5>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td><code>1</code></td>
<td>Page number (1-indexed)</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>integer</td>
<td>No</td>
<td><code>20</code></td>
<td>Items per page (max 100)</td>
</tr>
</tbody>
</table>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a901056d6b46ff92da0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;undo_stack_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;redo_stack_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;results&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;history_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">52</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;operation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;bind_trace&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;is_undone&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-05-27T14:00:00Z&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;undone_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;history_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">51</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;operation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;unbind&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;is_undone&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-05-27T13:00:00Z&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;undone_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-05-27T14:30:00Z&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;history_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">50</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;operation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;bind&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;is_undone&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-05-27T12:00:00Z&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;undone_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total bind history records for this identity</td>
</tr>
<tr>
<td><code>undo_stack_count</code></td>
<td>integer</td>
<td>Records available for undo (<code>is_undone=false</code>)</td>
</tr>
<tr>
<td><code>redo_stack_count</code></td>
<td>integer</td>
<td>Records available for redo (<code>is_undone=true</code>)</td>
</tr>
<tr>
<td><code>results[].history_id</code></td>
<td>integer</td>
<td>History record ID</td>
</tr>
<tr>
<td><code>results[].operation</code></td>
<td>string</td>
<td>Operation type (<code>bind</code>, <code>unbind</code>, or <code>bind_trace</code>)</td>
</tr>
<tr>
<td><code>results[].is_undone</code></td>
<td>boolean</td>
<td>Whether the operation has been undone</td>
</tr>
<tr>
<td><code>results[].created_at</code></td>
<td>string</td>
<td>When the operation was applied</td>
</tr>
<tr>
<td><code>results[].undone_at</code></td>
<td>string</td>
<td>When the undo occurred (null if not undone)</td>
</tr>
</tbody>
</table>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/bind/history?page=1&amp;limit=10&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>Identity not found</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Database error</td>
</tr>
</tbody>
</table>
<hr />
<h3>3. Merge History &amp; Undo/Redo</h3>
<p>Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.</p>
<h4>Merge Operation Overview</h4>
<table class="table">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Storage</td>
<td>MongoDB <code>identity_merge_history</code> collection</td>
</tr>
<tr>
<td>Snapshot</td>
<td>Full source identity state + target identity state + aliases/metadata diffs</td>
</tr>
<tr>
<td>Trigger</td>
<td>Every mergeinto with <code>keep_history=true</code></td>
</tr>
<tr>
<td>Undo deadline</td>
<td>24 hours (renewed on redo)</td>
</tr>
<tr>
<td>Redo support</td>
<td>Yes — restores undone merges with new 24hr deadline</td>
</tr>
<tr>
<td>Max records</td>
<td>Unlimited</td>
</tr>
</tbody>
</table>
<hr />
<h4><code>POST /api/v1/identity/merge/:merge_id/undo</code></h4>
<p>Already documented in <a href="07_identity.md#post-apiv1identitymergemerge_idundo"><code>07_identity.md</code></a>. See that document for full details.</p>
<hr />
<h4><code>POST /api/v1/identity/merge/:merge_id/redo</code></h4>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Redo a previously undone merge operation within the renewed 24-hour deadline.</p>
<h5>Request</h5>
<p>No body required. The merge ID is taken from the URL path.</p>
<h5>Behavior</h5>
<ol>
<li>Validates the merge record exists and <code>undone=true</code> (not already active)</li>
<li>Checks the 24-hour undo deadline (if expired, the redo is rejected)</li>
<li>Restores face bindings: moves all faces from <code>target_identity</code> back to <code>source_identity</code></li>
<li>Re-adds aliases that were removed by the undo (aliases with <code>source: "merge"</code> tag)</li>
<li>Re-adds metadata fields that were removed by the undo</li>
<li>If <code>keep_history=true</code>: sets <code>source_identity.status = 'merged'</code> again</li>
<li>If <code>keep_history=false</code>: recreates source identity from the <code>undone_snapshot</code> stored at undo time</li>
<li>Syncs both identity JSON files to disk</li>
<li>Sets <code>undone=false</code>, clears <code>undone_snapshot</code>, renews <code>undo_deadline = NOW() + 24h</code></li>
<li>Records <code>redone_by</code> user for audit</li>
</ol>
<h5>Example</h5>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h5>Response (200)</h5>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Redo merge completed: merged &#39;stranger_13894&#39; into &#39;Louis Viret&#39; (52 faces transferred)&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;data&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;merge_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;550e8400-e29b-41d4-a716-446655440000&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;faces_transferred&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">52</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;aliases_re_added&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;metadata_fields_re_added&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>merge_id</code></td>
<td>string</td>
<td>The merge operation ID</td>
</tr>
<tr>
<td><code>faces_transferred</code></td>
<td>integer</td>
<td>Number of faces transferred from source to target</td>
</tr>
<tr>
<td><code>aliases_re_added</code></td>
<td>integer</td>
<td>Number of aliases restored to target</td>
</tr>
<tr>
<td><code>metadata_fields_re_added</code></td>
<td>integer</td>
<td>Number of metadata fields restored to target</td>
</tr>
</tbody>
</table>
<h5>Error Responses</h5>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>400</code></td>
<td>Merge not undone, deadline expired, or cannot redo</td>
</tr>
<tr>
<td><code>404</code></td>
<td>Merge record not found</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Database error</td>
</tr>
</tbody>
</table>
<hr />
<h3>4. Delete History &amp; Undo/Redo</h3>
<h4>Delete Operation Overview</h4>
<table class="table">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Storage</td>
<td>PostgreSQL <code>identity_history</code> table</td>
</tr>
<tr>
<td>Snapshot</td>
<td><code>{"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}</code></td>
</tr>
<tr>
<td>Max records</td>
<td>1 active delete record per identity (redo stack cleared on new delete)</td>
</tr>
<tr>
<td>Undo support</td>
<td>Yes — recreates identity row, re-binds faces</td>
</tr>
<tr>
<td>Redo support</td>
<td>Yes — re-deletes the identity</td>
</tr>
<tr>
<td>Identity file</td>
<td>Deleted on delete, recreated on undo</td>
</tr>
</tbody>
</table>
<h4>Snapshot Format</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;identity&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;a9a90105-6d6b-46ff-92da-0c3c1a57dff4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Cary Grant&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;identity_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;people&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;source&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;tmdb&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;confirmed&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;metadata&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nt">&quot;tmdb_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">112</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;tmdb_profile&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;unbound_faces&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;aeed71342a899fe4b4c57b7d41bcb692&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1_6&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">906</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<h4>Stack Model</h4>
<div class="codehilite"><pre><span></span><code>DELETE identity (undo stack, is_undone=false)
↓ undo
Identity recreated, faces re-bound
→ delete history marked is_undone=true
↓ redo (re-delete)
Identity deleted again, faces unbound
→ delete history marked is_undone=false
</code></pre></div>
<p>A new delete after an undo clears the delete redo stack (no redo possible for the old delete).</p>
<h4>Undo Behavior (via existing <code>POST /api/v1/identity/:identity_uuid/undo</code>)</h4>
<ol>
<li>Normal identity lookup fails (row was deleted)</li>
<li>Checks <code>identity_history</code> for <code>operation='delete' AND is_undone=false</code> matching the UUID in the snapshot</li>
<li>Recreates the identity row (new internal <code>id</code>, same UUID)</li>
<li>Re-binds all faces listed in <code>unbound_faces</code> to the new identity</li>
<li>Deletes the <code>identity_history</code> delete record as <code>is_undone=true</code> with <code>undone_at=NOW()</code></li>
<li>Syncs <code>identity.json</code> to disk</li>
<li>Updates <code>_index.json</code></li>
</ol>
<h4>Redo Behavior (via existing <code>POST /api/v1/identity/:identity_uuid/redo</code>)</h4>
<ol>
<li>Identity lookup succeeds (identity was restored by prior undo)</li>
<li>Checks <code>identity_history</code> for <code>operation='delete' AND is_undone=true</code> matching the identity_id</li>
<li>Deletes <code>identity.json</code> from disk</li>
<li>Unbinds all faces (<code>identity_id = NULL</code>)</li>
<li>Deletes the identity row</li>
<li>Marks the delete history record as <code>is_undone=false</code></li>
<li>Returns success</li>
</ol>
<h4>Error Responses (delete undo/redo)</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>Scenario</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>400</code></td>
<td>No delete history available (either no delete or already undone/redone)</td>
</tr>
<tr>
<td><code>404</code></td>
<td>Identity not found (for redo — identity wasn't restored)</td>
</tr>
<tr>
<td><code>500</code></td>
<td>Database error</td>
</tr>
</tbody>
</table>
<hr />
<h3>Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo</h3>
<table class="table">
<thead>
<tr>
<th>Aspect</th>
<th>PATCH Undo/Redo</th>
<th>Merge Undo</th>
<th>Bind Undo/Redo</th>
<th>Merge Undo/Redo</th>
<th>Delete Undo/Redo</th>
</tr>
</thead>
<tbody>
<tr>
<td>Storage</td>
<td>PostgreSQL <code>identity_history</code></td>
<td>PostgreSQL <code>identity_history</code></td>
<td>MongoDB <code>identity_merge_history</code></td>
<td>PostgreSQL <code>identity_history</code></td>
</tr>
<tr>
<td>Operation filter</td>
<td><code>operation='update'</code></td>
<td><code>operation IN ('bind','unbind','bind_trace')</code></td>
<td></td>
<td><code>operation='delete'</code></td>
</tr>
<tr>
<td>Trigger</td>
<td>Every PATCH</td>
<td>Every bind/unbind/bind_trace</td>
<td>Every mergeinto with <code>keep_history=true</code></td>
<td>Every DELETE</td>
</tr>
<tr>
<td>Undo deadline</td>
<td>None (unlimited)</td>
<td>24 hours</td>
<td>None (unlimited)</td>
<td>24 hours (renewed on redo)</td>
<td>None (unlimited)</td>
</tr>
<tr>
<td>Redo support</td>
<td>Yes</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Step undo</td>
<td>Yes (<code>steps</code> param)</td>
<td>No (full undo only)</td>
<td>Yes (<code>steps</code> param)</td>
<td>No (full undo/redo only)</td>
<td>No (single record)</td>
</tr>
<tr>
<td>Max records</td>
<td>256 per identity</td>
<td>256 per identity (shared)</td>
<td>Unlimited</td>
<td>256 per identity (shared)</td>
</tr>
<tr>
<td>User tracking</td>
<td><code>user_id</code> + <code>user_source</code></td>
<td><code>user_id</code> + <code>user_source</code></td>
<td><code>performed_by_user</code> + <code>undone_by</code> / <code>redone_by</code></td>
<td><code>user_id</code> + <code>user_source</code></td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,728 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>15 Tkg - Momentry API Docs</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
h1 { font-size: 24px; margin: 24px 0 12px; }
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
p { line-height: 1.6; margin: 8px 0; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f0f0f0; font-weight: 600; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
pre code { background: none; padding: 0; }
a { color: #0066cc; }
.back { display: inline-block; margin-bottom: 20px; color: #666; }
.back:hover { color: #333; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.logout-btn { font-size: 13px; color: #999; text-decoration: none; }
.logout-btn:hover { color: #cc0000; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="back" href="index.html">&larr; Back to index</a>
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: tkg -->
<!-- description: Temporal Knowledge Graph — rebuild, nodes, edges, processor counts -->
<!-- depends: 05_process, 07_identity -->
<h2>Temporal Knowledge Graph (TKG)</h2>
<p>TKG is a time-aligned knowledge graph built from multi-processor outputs (face, yolo, ocr, pose, asrx, gaze, lip, appearance). It produces 9 node types and 14 edge types stored in <code>dev.tkg_nodes</code> and <code>dev.tkg_edges</code>.</p>
<h3>Node Types</h3>
<table class="table">
<thead>
<tr>
<th>Node Type</th>
<th>Description</th>
<th>Key Properties</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>face_trace</code></td>
<td>A tracked face identity over time</td>
<td><code>trace_id</code>, <code>face_count</code>, <code>avg_confidence</code></td>
</tr>
<tr>
<td><code>gaze_trace</code></td>
<td>Gaze direction over time</td>
<td><code>direction</code> (frontal/left/right/up/down + diagonals)</td>
</tr>
<tr>
<td><code>lip_trace</code></td>
<td>Lip movement synced with speech</td>
<td><code>speaker_id</code>, <code>lip_area_range</code></td>
</tr>
<tr>
<td><code>text_trace</code></td>
<td>Spoken text aligned to time</td>
<td><code>speaker_id</code>, <code>text</code>, <code>start_time</code>, <code>end_time</code></td>
</tr>
<tr>
<td><code>appearance_trace</code></td>
<td>Human appearance (clothing) over time</td>
<td><code>clothing_color</code>, <code>upper_cloth</code>, <code>lower_cloth</code></td>
</tr>
<tr>
<td><code>skin_tone_trace</code></td>
<td>Fitzpatrick skin tone classification</td>
<td><code>fitzpatrick_type</code> (IVI)</td>
</tr>
<tr>
<td><code>accessory</code></td>
<td>Detected accessories</td>
<td><code>type</code> (glasses/hat/etc.), <code>confidence</code></td>
</tr>
<tr>
<td><code>object</code></td>
<td>YOLO-detected object</td>
<td><code>class</code>, <code>confidence</code>, <code>frame_count</code></td>
</tr>
<tr>
<td><code>speaker</code></td>
<td>ASRX speaker segment</td>
<td><code>speaker_id</code>, <code>segment_count</code>, <code>total_duration</code></td>
</tr>
</tbody>
</table>
<h3>Edge Types</h3>
<table class="table">
<thead>
<tr>
<th>Edge Type</th>
<th>Source → Target</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>co_occurs</code></td>
<td>object ↔ object</td>
<td>Two objects appear together in same frame</td>
</tr>
<tr>
<td><code>speaker_face</code></td>
<td>speaker ↔ face_trace</td>
<td>Speaker matched to face trace via lip sync</td>
</tr>
<tr>
<td><code>face_face</code></td>
<td>face_trace ↔ face_trace</td>
<td>Two face traces interact (mutual gaze)</td>
</tr>
<tr>
<td><code>mutual_gaze</code></td>
<td>gaze_trace ↔ gaze_trace</td>
<td>Two people looking at each other</td>
</tr>
<tr>
<td><code>lip_sync</code></td>
<td>lip_trace ↔ text_trace</td>
<td>Lip movement aligned with spoken text</td>
</tr>
<tr>
<td><code>has_appearance</code></td>
<td>face_trace ↔ appearance_trace</td>
<td>Face has specific appearance</td>
</tr>
<tr>
<td><code>wears</code></td>
<td>face_trace ↔ accessory</td>
<td>Face wears an accessory</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/tkg/rebuild</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Rebuild the Temporal Knowledge Graph for a file. Reads processor JSON outputs (face, yolo, ocr, pose, asrx, gaze, lip, appearance) and generates TKG nodes and edges. Clears existing nodes/edges for the file first, then rebuilds from scratch.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/rebuild&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;result&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;face_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;gaze_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;lip_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;text_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">24</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;appearance_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;skin_tone_trace_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;accessory_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;object_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">26</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;speaker_nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;co_occurrence_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">94</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;speaker_face_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_face_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;mutual_gaze_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;lip_sync_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_appearance_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;wears_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>True if rebuild completed</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>result</code></td>
<td>object</td>
<td>Node and edge counts by type</td>
</tr>
<tr>
<td><code>error</code></td>
<td>string/null</td>
<td>Error message if failed</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/tkg/nodes</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Query TKG nodes with pagination and optional type filter.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>node_type</code></td>
<td>string</td>
<td>No</td>
<td>all</td>
<td>Filter by node type: <code>face_trace</code>, <code>gaze_trace</code>, <code>lip_trace</code>, <code>text_trace</code>, <code>appearance_trace</code>, <code>skin_tone_trace</code>, <code>accessory</code>, <code>object</code>, <code>speaker</code></td>
</tr>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td>1</td>
<td>Page number</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>No</td>
<td>100</td>
<td>Items per page (max 500)</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code><span class="c1"># Get all face_trace nodes</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/nodes&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;node_type&quot;: &quot;face_trace&quot;, &quot;page&quot;: 1, &quot;page_size&quot;: 50}&#39;</span>
<span class="c1"># Get all nodes</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/nodes&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">50</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;nodes&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;node_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face_trace&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;external_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;trace_0&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Face Trace 0&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;properties&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">142</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;avg_confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.87</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total matching node count</td>
</tr>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>Current page</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>Items per page</td>
</tr>
<tr>
<td><code>nodes</code></td>
<td>array</td>
<td>Array of node objects</td>
</tr>
<tr>
<td><code>nodes[].id</code></td>
<td>integer</td>
<td>Database primary key</td>
</tr>
<tr>
<td><code>nodes[].node_type</code></td>
<td>string</td>
<td>Node type (see table above)</td>
</tr>
<tr>
<td><code>nodes[].external_id</code></td>
<td>string</td>
<td>External identifier (e.g., <code>trace_0</code>, <code>gaze_1</code>)</td>
</tr>
<tr>
<td><code>nodes[].label</code></td>
<td>string</td>
<td>Human-readable label</td>
</tr>
<tr>
<td><code>nodes[].properties</code></td>
<td>object</td>
<td>Type-specific properties as JSON</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/tkg/edges</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Query TKG edges with pagination and optional filters.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>edge_type</code></td>
<td>string</td>
<td>No</td>
<td>all</td>
<td>Filter by edge type: <code>co_occurs</code>, <code>speaker_face</code>, <code>face_face</code>, <code>mutual_gaze</code>, <code>lip_sync</code>, <code>has_appearance</code>, <code>wears</code></td>
</tr>
<tr>
<td><code>source_type</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by source node type</td>
</tr>
<tr>
<td><code>target_type</code></td>
<td>string</td>
<td>No</td>
<td></td>
<td>Filter by target node type</td>
</tr>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>No</td>
<td>1</td>
<td>Page number</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>No</td>
<td>100</td>
<td>Items per page (max 500)</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code><span class="c1"># Get all co_occurrence edges</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/edges&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;edge_type&quot;: &quot;co_occurs&quot;}&#39;</span>
<span class="c1"># Get edges between face_trace and speaker nodes</span>
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/edges&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;source_type&quot;: &quot;speaker&quot;, &quot;target_type&quot;: &quot;face_trace&quot;}&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">94</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;page_size&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;edge_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;co_occurs&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;source_node_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;target_node_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;properties&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.92</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>total</code></td>
<td>integer</td>
<td>Total matching edge count</td>
</tr>
<tr>
<td><code>page</code></td>
<td>integer</td>
<td>Current page</td>
</tr>
<tr>
<td><code>page_size</code></td>
<td>integer</td>
<td>Items per page</td>
</tr>
<tr>
<td><code>edges</code></td>
<td>array</td>
<td>Array of edge objects</td>
</tr>
<tr>
<td><code>edges[].id</code></td>
<td>integer</td>
<td>Database primary key</td>
</tr>
<tr>
<td><code>edges[].edge_type</code></td>
<td>string</td>
<td>Edge type</td>
</tr>
<tr>
<td><code>edges[].source_node_id</code></td>
<td>integer</td>
<td>Source node ID (FK to tkg_nodes)</td>
</tr>
<tr>
<td><code>edges[].target_node_id</code></td>
<td>integer</td>
<td>Target node ID (FK to tkg_nodes)</td>
</tr>
<tr>
<td><code>edges[].properties</code></td>
<td>object</td>
<td>Edge-specific properties as JSON</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/tkg/node/:node_id</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get detail for a specific TKG node including its connected edges.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tkg/node/1&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;node&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;node_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face_trace&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;external_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;trace_0&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Face Trace 0&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;properties&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">142</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;avg_confidence&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">0.87</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;connected_edges&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;edge_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;co_occurs&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;source_node_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;target_node_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;properties&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">&quot;edge_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>success</code></td>
<td>boolean</td>
<td>Always true on 200</td>
</tr>
<tr>
<td><code>node</code></td>
<td>object</td>
<td>Node detail (same format as nodes query)</td>
</tr>
<tr>
<td><code>connected_edges</code></td>
<td>array</td>
<td>Edges connected to this node</td>
</tr>
<tr>
<td><code>edge_count</code></td>
<td>integer</td>
<td>Total connected edge count</td>
</tr>
</tbody>
</table>
<h4>Error Codes</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>Node not found</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/processor-counts</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Get counts of processor JSON output files for a file. Scans the output directory for <code>{file_uuid}.{processor}.json</code> files and extracts frame counts, segment counts, and chunk counts from each file.</p>
<p>Supports short UUID prefix matching (e.g., <code>d3f9ae8e</code> → resolves to full <code>d3f9ae8e471a1fc4d47022c66091b920</code>).</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/processor-counts&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;output_dir&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/Users/accusys/momentry/output_dev&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;processors&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;processor&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;cut&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_json&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5391</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;segment_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunk_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;last_modified&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-06-16T18:48:01.987241061+00:00&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;processor&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_json&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1112</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;segment_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunk_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;last_modified&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-06-18T17:21:37.408383765+00:00&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;processor&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;asrx&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_json&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;segment_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunk_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;last_modified&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-06-18T17:21:40.872063642+00:00&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;processor&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;story&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_json&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;segment_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunk_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;last_modified&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-06-18T17:22:00.000000000+00:00&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;processor&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;mediapipe&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;has_json&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;segment_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;chunk_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;last_modified&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Full 32-char hex UUID (resolved from prefix)</td>
</tr>
<tr>
<td><code>output_dir</code></td>
<td>string</td>
<td>Output directory scanned</td>
</tr>
<tr>
<td><code>processors</code></td>
<td>array</td>
<td>Per-processor output info</td>
</tr>
<tr>
<td><code>processors[].processor</code></td>
<td>string</td>
<td>Processor name</td>
</tr>
<tr>
<td><code>processors[].has_json</code></td>
<td>boolean</td>
<td>Whether JSON file exists</td>
</tr>
<tr>
<td><code>processors[].frame_count</code></td>
<td>integer/null</td>
<td>Total frames processed (frame-based processors)</td>
</tr>
<tr>
<td><code>processors[].segment_count</code></td>
<td>integer/null</td>
<td>Segment count (ASRX segments, etc.)</td>
</tr>
<tr>
<td><code>processors[].chunk_count</code></td>
<td>integer/null</td>
<td>Chunk count (Story chunks, etc.)</td>
</tr>
<tr>
<td><code>processors[].last_modified</code></td>
<td>string/null</td>
<td>ISO 8601 timestamp of last modification</td>
</tr>
</tbody>
</table>
<h4>Error Codes</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>File UUID not found in database</td>
</tr>
</tbody>
</table>
<hr />
<p><em>Updated: 2026-06-20 12:00:00</em></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>16 Workspace - Momentry API Docs</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
h1 { font-size: 24px; margin: 24px 0 12px; }
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
p { line-height: 1.6; margin: 8px 0; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f0f0f0; font-weight: 600; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
pre code { background: none; padding: 0; }
a { color: #0066cc; }
.back { display: inline-block; margin-bottom: 20px; color: #666; }
.back:hover { color: #333; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.logout-btn { font-size: 13px; color: #999; text-decoration: none; }
.logout-btn:hover { color: #cc0000; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="back" href="index.html">&larr; Back to index</a>
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: workspace -->
<!-- description: Workspace checkout/checkin — lock, clear, restore file data -->
<!-- depends: 04_lookup, 05_process -->
<h2>Workspace Checkin/Checkout</h2>
<p>Workspace checkin/checkout provides a transactional editing model for file data:
- <strong>Checkout</strong>: Clears PG tables (face_detections, speaker_detections, pre_chunks) and Qdrant vectors, creating an isolated workspace SQLite for editing.
- <strong>Checkin</strong>: Restores data from the workspace SQLite back to PG and Qdrant, marking the file as <code>Indexed</code>.</p>
<p>This allows safe concurrent editing — while a file is checked out, its main database records are cleared, preventing conflicts.</p>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/checkout</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Checkout a file workspace. Clears face detections, speaker detections, pre_chunks from PostgreSQL, deletes Qdrant vectors, and creates a workspace SQLite database for isolated editing.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/checkout&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;rows_deleted&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1523</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;checked_out&quot;</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>rows_deleted</code></td>
<td>integer</td>
<td>Total rows cleared from PG tables</td>
</tr>
<tr>
<td><code>status</code></td>
<td>string</td>
<td><code>"checked_out"</code></td>
</tr>
</tbody>
</table>
<h4>Error Responses</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>500</code></td>
<td>Checkout failed (DB error, workspace creation error)</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>POST /api/v1/file/:file_uuid/checkin</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Checkin a file workspace. Restores face detections, speaker detections, pre_chunks from workspace SQLite back to PostgreSQL, re-indexes vectors to Qdrant, and sets video status to <code>Indexed</code>.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/checkin&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;pre_chunks_moved&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;face_detections_moved&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;speaker_detections_moved&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">320</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;vectors_moved&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;indexed&quot;</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>pre_chunks_moved</code></td>
<td>integer</td>
<td>Pre-chunks restored from workspace</td>
</tr>
<tr>
<td><code>face_detections_moved</code></td>
<td>integer</td>
<td>Face detections restored from workspace</td>
</tr>
<tr>
<td><code>speaker_detections_moved</code></td>
<td>integer</td>
<td>Speaker detections restored from workspace</td>
</tr>
<tr>
<td><code>vectors_moved</code></td>
<td>integer</td>
<td>Vectors re-indexed to Qdrant</td>
</tr>
<tr>
<td><code>status</code></td>
<td>string</td>
<td><code>"indexed"</code></td>
</tr>
</tbody>
</table>
<h4>Error Responses</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>500</code></td>
<td>Checkin failed (DB error, workspace not found, vector index error)</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>GET /api/v1/file/:file_uuid/workspace</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Check if a workspace SQLite database exists for a file.</p>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/workspace&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;d3f9ae8e471a1fc4d47022c66091b920&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;exists&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>32-char hex UUID</td>
</tr>
<tr>
<td><code>exists</code></td>
<td>boolean</td>
<td>True if workspace SQLite exists</td>
</tr>
</tbody>
</table>
<hr />
<h3>Workflow</h3>
<div class="codehilite"><pre><span></span><code> REGISTERED ──→ CHECKED_OUT ──→ INDEXED
│ │ │
│ checkout checkin
│ │ │
│ clear PG + Qdrant restore from SQLite
│ create workspace re-index vectors
│ set status set status
</code></pre></div>
<ol>
<li><strong>Register</strong> file → status: <code>REGISTERED</code></li>
<li><strong>Process</strong> file → processors run, data stored in PG + Qdrant</li>
<li><strong>Checkout</strong> file → clear editable data, create workspace SQLite → status: <code>CHECKED_OUT</code></li>
<li><strong>Edit</strong> workspace via Agent Search / identity binding</li>
<li><strong>Checkin</strong> file → restore from workspace SQLite → status: <code>INDEXED</code></li>
<li><strong>Rebuild TKG</strong> if needed after checkin</li>
</ol>
<hr />
<p><em>Updated: 2026-06-20 12:00:00</em></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>99 Incomplete - Momentry API Docs</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
h1 { font-size: 24px; margin: 24px 0 12px; }
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
p { line-height: 1.6; margin: 8px 0; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f0f0f0; font-weight: 600; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
pre code { background: none; padding: 0; }
a { color: #0066cc; }
.back { display: inline-block; margin-bottom: 20px; color: #666; }
.back:hover { color: #333; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.logout-btn { font-size: 13px; color: #999; text-decoration: none; }
.logout-btn:hover { color: #cc0000; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="back" href="index.html">&larr; Back to index</a>
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: incomplete -->
<!-- description: Incomplete, stub, or undocumented API endpoints — tracking list -->
<!-- depends: 01_auth -->
<h2>Incomplete / Undocumented APIs</h2>
<p>This module tracks API endpoints that exist in the codebase but are either undocumented, partially documented, or stubs.</p>
<blockquote>
<p><strong>Note</strong>: Endpoints listed here should be fully documented and moved to their appropriate module once implemented.</p>
</blockquote>
<hr />
<h2>Identity Binding</h2>
<h3><code>POST /api/v1/identity/:identity_uuid/bind</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: identity-level</p>
<p>Bind a single face detection to an identity. Unlike <code>bind/trace</code> which binds all faces in a trace, this binds one specific face.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File containing the face</td>
</tr>
<tr>
<td><code>face_id</code></td>
<td>string</td>
<td>Yes</td>
<td>Face detection ID to bind</td>
</tr>
</tbody>
</table>
<h4>Status</h4>
<p>⚠️ <strong>Undocumented</strong> — exists in code but no full request/response documentation.</p>
<hr />
<h2>Resource Management</h2>
<h3><code>POST /api/v1/resource/register</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Register an external resource (e.g., storage backend, API service).</p>
<h4>Status</h4>
<p>⚠️ <strong>Undocumented</strong> — endpoint exists but no documentation.</p>
<hr />
<h3><code>POST /api/v1/resource/heartbeat</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Send heartbeat for a registered resource to verify it's still alive.</p>
<h4>Status</h4>
<p>⚠️ <strong>Undocumented</strong> — endpoint exists but no documentation.</p>
<hr />
<h3><code>GET /api/v1/resources</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>List all registered resources with their status.</p>
<h4>Status</h4>
<p>⚠️ <strong>Undocumented</strong> — endpoint exists but no documentation.</p>
<hr />
<h2>5W1H Agent</h2>
<h3><code>POST /api/v1/agents/5w1h/analyze</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Run 5W1H analysis on all cut scenes for a file. Uses LLM (Gemma4) to summarize each scene with who/what/where/when/why/how.</p>
<h4>Status</h4>
<p>⚠️ <strong>Partially documented</strong> — listed in <code>12_agent.md</code> but missing full request/response examples.</p>
<hr />
<h3><code>POST /api/v1/agents/5w1h/batch</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Run 5W1H analysis on multiple files at once.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuids</code></td>
<td>string[]</td>
<td>Yes</td>
<td>Array of file UUIDs to analyze</td>
</tr>
</tbody>
</table>
<h4>Status</h4>
<p>⚠️ <strong>Partially documented</strong> — listed in <code>12_agent.md</code> but missing full request/response examples.</p>
<hr />
<h3><code>GET /api/v1/agents/5w1h/status</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Get 5W1H analysis status across all videos (which files have been analyzed, which are pending).</p>
<h4>Status</h4>
<p>⚠️ <strong>Partially documented</strong> — listed in <code>12_agent.md</code> but missing full response schema.</p>
<hr />
<h2>Identity Agent</h2>
<h3><code>POST /api/v1/agents/identity/match-from-photo</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: system-level</p>
<p>Match an identity using an uploaded photo. Extracts face embedding, finds best trace match.</p>
<h4>Status</h4>
<p>⚠️ <strong>Partially documented</strong> — exists in <code>08_identity_agent.md</code> but missing full response schema and error cases.</p>
<hr />
<h3><code>POST /api/v1/agents/identity/match-from-trace</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Match an identity using a trace. Multi-angle embedding comparison with propagation.</p>
<h4>Status</h4>
<p>⚠️ <strong>Partially documented</strong> — exists in <code>08_identity_agent.md</code> but missing full response schema and error cases.</p>
<hr />
<h2>Stubs / Not Implemented</h2>
<h3>Visual Search Endpoints</h3>
<table class="table">
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual</code></td>
<td>Stub — defined but not functional</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/class</code></td>
<td>Stub — defined but not functional</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/density</code></td>
<td>Stub — defined but not functional</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/combination</code></td>
<td>Stub — defined but not functional</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/v1/search/visual/stats</code></td>
<td>Stub — defined but not functional</td>
</tr>
</tbody>
</table>
<h3>Unmounted Routes</h3>
<p>These endpoints are defined in source code but not mounted in the router:</p>
<table class="table">
<thead>
<tr>
<th>Endpoint</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/api/v1/search/persons</code></td>
<td>Defined but not mounted</td>
</tr>
<tr>
<td><code>/api/v1/who</code></td>
<td>Defined but not mounted</td>
</tr>
<tr>
<td><code>/api/v1/who/candidates</code></td>
<td>Defined but not mounted</td>
</tr>
</tbody>
</table>
<hr />
<h2>Tracking</h2>
<table class="table">
<thead>
<tr>
<th>Count</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Undocumented</td>
<td>3 (resource management)</td>
</tr>
<tr>
<td>Partially documented</td>
<td>5 (5W1H ×3, identity agent ×2)</td>
</tr>
<tr>
<td>Stub/not functional</td>
<td>5 (visual search)</td>
</tr>
<tr>
<td>Defined but unmounted</td>
<td>3 (persons, who, who/candidates)</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>16</strong></td>
</tr>
</tbody>
</table>
<hr />
<p><em>Created: 2026-06-20 — Gap analysis from core API vs doc_wasm sync</em>
<em>Updated: 2026-06-20 — Initial tracking list</em></p>
</div>
</body>
</html>

View File

@@ -29,7 +29,7 @@ a:hover td { background: #f8f8f8; border-radius: 4px; }
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<p class="subtitle">API 參考手冊 — 登入後可瀏覽各模組文件</p>
<table><tr onclick="window.location='11_error_codes.html'" style="cursor:pointer"><td class="cn">錯誤碼</td><td class="en">Error Codes</td></tr><tr onclick="window.location='14_identity_history.html'" style="cursor:pointer"><td class="cn">14 Identity History</td><td class="en"></td></tr></table>
<table><tr onclick="window.location='11_error_codes.html'" style="cursor:pointer"><td class="cn">錯誤碼</td><td class="en">Error Codes</td></tr><tr onclick="window.location='14_identity_history.html'" style="cursor:pointer"><td class="cn">14 Identity History</td><td class="en"></td></tr><tr onclick="window.location='15_tkg.html'" style="cursor:pointer"><td class="cn">15 Tkg</td><td class="en"></td></tr><tr onclick="window.location='16_workspace.html'" style="cursor:pointer"><td class="cn">16 Workspace</td><td class="en"></td></tr><tr onclick="window.location='99_incomplete.html'" style="cursor:pointer"><td class="cn">99 Incomplete</td><td class="en"></td></tr></table>
</div>
</body>
</html>

View File

@@ -127,13 +127,15 @@ curl -s "$API/api/v1/file/$FILE_UUID/probe" -H "X-API-Key: $KEY"
---
### `GET /api/v1/progress/:file_uuid`
### `POST /api/v1/progress/:file_uuid`
**Auth**: Required
**Scope**: file-level
Get real-time processing progress for a file via Redis pub/sub. Includes per-processor status, current/total frames, ETA, and system resource stats.
**Note**: This endpoint uses **POST** method, not GET. The progress data is stored in Redis as a hash, and POST is used to retrieve the latest state.
#### Pipeline Order
| Order | Processor | Dependencies | Description |
@@ -154,7 +156,7 @@ All processors except `story` and `5w1h` run concurrently when their dependencie
#### Example
```bash
curl -s "$API/api/v1/progress/$FILE_UUID" -H "X-API-Key: $KEY" | jq '{overall_progress, processors: [.processors[] | {processor_type, status}]}'
curl -s -X POST "$API/api/v1/progress/$FILE_UUID" -H "X-API-Key: $KEY" | jq '{overall_progress, processors: [.processors[] | {name, status}]}'
```
#### Response (200)

View File

@@ -923,6 +923,128 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID/json" \
---
---
### `POST /api/v1/file/:file_uuid/pending-person`
**Auth**: Required
**Scope**: file-level
Create a manually managed "pending person" under a specific file. A pending person is an identity with `status='pending'` and `source='manual'`, used for unmatched traces that the user wants to manually label before a full identity resolution.
Optionally binds a list of trace IDs to this new identity.
#### Request
```json
{
"trace_ids": [100, 150, 200],
"name": "Mystery Man #1"
}
```
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `trace_ids` | array[int] | No | `[]` | Trace IDs to bind to this pending person |
| `name` | string | No | `"Person N"` | Human-readable name. Auto-generated if omitted |
#### Example
```bash
# Create pending person with name and no traces
curl -s -X POST "$API/api/v1/file/$FILE_UUID/pending-person" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Unknown Woman #2", "trace_ids": []}'
# Create pending person with auto-name and bind traces
curl -s -X POST "$API/api/v1/file/$FILE_UUID/pending-person" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"trace_ids": [100, 150, 200]}'
```
#### Response (200)
```json
{
"success": true,
"message": "Created pending person: Mystery Man #1 (uuid: 4d96b25b-68f0-4c52-b238-d69f7dfd588b)",
"data": {
"identity_uuid": "4d96b25b-68f0-4c52-b238-d69f7dfd588b",
"identity_id": 55,
"name": "Mystery Man #1",
"bound_traces": 0
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `identity_uuid` | string | UUID of the newly created pending identity |
| `identity_id` | integer | Internal ID of the new identity |
| `name` | string | Display name |
| `bound_traces` | integer | Number of traces bound |
#### Side Effects
- Creates an `identities` row with `status='pending'`, `source='manual'`, `file_uuid=<file_uuid>`
- If `trace_ids` provided: `UPDATE face_detections SET identity_id = ...` for matching traces
- If `trace_ids` provided: TKG face_track nodes get `identity_id` / `identity_name` in properties
- Identity JSON file synced to disk
---
### `GET /api/v1/file/:file_uuid/pending-persons`
**Auth**: Required
**Scope**: file-level
List all pending persons for a file.
#### Example
```bash
curl -s "$API/api/v1/file/$FILE_UUID/pending-persons" \
-H "X-API-Key: $KEY"
```
#### Response (200)
```json
{
"success": true,
"message": "Found 2 pending persons for c36f35685177c981aa139b66bbbccc5b",
"data": [
{
"identity_uuid": "232ecd08-a2bf-4bd0-bd25-0bd8fb7a7dae",
"identity_id": 56,
"name": "Person 2",
"created_at": "2026-06-23 17:13:23",
"trace_count": 3,
"bound_traces": null
}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `identity_uuid` | string | Identity UUID |
| `identity_id` | integer | Internal identity ID |
| `name` | string | Display name |
| `created_at` | string | Creation timestamp |
| `trace_count` | integer | Number of face traces bound to this pending person |
| `bound_traces` | array[int] | List of bound trace IDs (currently null, reserved for future expansion) |
#### Notes
- Pending persons are normal `identities` rows with `status='pending'` — they can be promoted to confirmed via `PATCH /api/v1/identity/:identity_uuid` (`{"status": "confirmed"}`)
- They can be merged into known identities via `POST /api/v1/identity/:identity_uuid/mergeinto`
- Use `GET /api/v1/identity/:identity_uuid/traces` to get detailed trace info for each pending person
---
## Alias System (BCP 47 Locale Tags)
Identity aliases support multilingual display names. Aliases are stored in `metadata.aliases` as an array of `{locale, name}` objects.