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

@@ -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>