feat: ASRX hybrid pipeline, identity history, worker fixes, checkpoint system
This commit is contained in:
2
docs_v1.0/API_WORKSPACE/.gitignore
vendored
Normal file
2
docs_v1.0/API_WORKSPACE/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
_build/
|
||||
.DS_Store
|
||||
60
docs_v1.0/API_WORKSPACE/README.md
Normal file
60
docs_v1.0/API_WORKSPACE/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# API Workspace
|
||||
|
||||
## Purpose
|
||||
|
||||
This directory is the **single source of truth** for all API documentation modules.
|
||||
Generated outputs go to `../GUIDES/` as assembled deliverable documents.
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
# 1. Edit a module
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 2. Preview the generated output
|
||||
make _build/API_ENDPOINTS.md
|
||||
|
||||
# 3. Check diff against current GUIDES/ content
|
||||
make check
|
||||
|
||||
# 4. Deploy to GUIDES/
|
||||
make deploy
|
||||
|
||||
# 5. Regenerate all
|
||||
make all
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
API_WORKSPACE/
|
||||
├── modules/ ← 11 module files (01_auth ... 11_error_codes)
|
||||
├── configs/ ← 7 assembly recipies (.toml)
|
||||
├── narratives/ ← narrative intros for specific output files
|
||||
├── _build/ ← generated output (gitignored)
|
||||
├── Makefile ← build targets
|
||||
├── assemble_docs.sh ← assembly engine
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Available `make` Targets
|
||||
|
||||
| Target | Output |
|
||||
|--------|--------|
|
||||
| `make reference` | `_build/API_REFERENCE.md` |
|
||||
| `make endpoints` | `_build/API_ENDPOINTS.md` |
|
||||
| `make quickref` | `_build/API_QUICK_REFERENCE.md` |
|
||||
| `make errors` | `_build/API_ERROR_CODES.md` |
|
||||
| `make index` | `_build/API_INDEX.md` |
|
||||
| `make marcom` | `_build/API_TRAINING_MARCOM.md` |
|
||||
| `make tmdb` | `_build/TMDb_User_Guide.md` |
|
||||
| `make all` | All of the above |
|
||||
| `make deploy` | Copy `_build/*` → `../GUIDES/` |
|
||||
| `make check` | `diff` against existing `../GUIDES/` files |
|
||||
|
||||
## Adding a New Endpoint
|
||||
|
||||
1. Add the endpoint to the appropriate module (e.g., `modules/XX_files.md`)
|
||||
2. Follow the template in `modules/_template.md`
|
||||
3. `make all && make check`
|
||||
4. `make deploy`
|
||||
@@ -7,7 +7,7 @@
|
||||
### `POST /api/v1/search/smart`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector `story_parent` and `llm_parent` chunks by cosine similarity.
|
||||
|
||||
@@ -15,13 +15,22 @@ Semantic vector search using EmbeddingGemma-300m. Generates a query embedding vi
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | Yes | — | File UUID to search within |
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | File UUID to search within. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 5 | Max results to return |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 5 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Audrey Hepburn"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
@@ -37,6 +46,7 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
"query": "Audrey Hepburn",
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"parent_id": 1087822,
|
||||
"scene_order": 1087822,
|
||||
"start_frame": 104438,
|
||||
@@ -54,12 +64,16 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where result was found |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/universal`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`.
|
||||
|
||||
@@ -68,13 +82,22 @@ Multi-type BM25 full-text search across chunks, frames, and persons. Uses Postgr
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `types` | string[] | No | `["chunk","frame","person"]` | Search types |
|
||||
| `limit` | integer | No | 10 | Max results per type |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Cary Grant"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
@@ -90,6 +113,7 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"results": [
|
||||
{
|
||||
"type": "chunk",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "story_child",
|
||||
"start_frame": 5103,
|
||||
@@ -98,6 +122,25 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"end_time": 213.64,
|
||||
"text": "[213s-214s] Cary Grant: \"Olá!\"",
|
||||
"score": 0.9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"frame_number": 5105,
|
||||
"timestamp": 212.72,
|
||||
"score": 0.7,
|
||||
"objects": null,
|
||||
"ocr_texts": null,
|
||||
"faces": null
|
||||
},
|
||||
{
|
||||
"type": "person",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"identity_id": 12,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"appearance_count": 542,
|
||||
"score": 0.95
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
@@ -105,23 +148,78 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].type` | string | Result type: `chunk`, `frame`, or `person` |
|
||||
| `results[].file_uuid` | string | File UUID where result was found (all types) |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/frames`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search face detection frames by identity name or trace ID.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/identity_text`
|
||||
### `GET /api/v1/search/identity_text`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search text chunks spoken by a specific identity.
|
||||
Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 50 | Items per page |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?file_uuid=$FILE_UUID&q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "llm_parent_..._256_270",
|
||||
"start_time": 256.256,
|
||||
"end_time": 270.228,
|
||||
"text_content": "...lack of affection...",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 94
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where chunk was found |
|
||||
| `results[].identity_id` | integer | Identity ID if face was detected |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,4 +243,4 @@ Search text chunks spoken by a specific identity.
|
||||
| **Storage** | pgvector (`chunk.embedding` column) |
|
||||
|
||||
---
|
||||
*Updated: 2026-05-19 12:49:24*
|
||||
*Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs*
|
||||
|
||||
@@ -70,7 +70,16 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID" -H "X-API-Key: $KEY"
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Delete an identity permanently.
|
||||
Delete an identity permanently. All face detections bound to this identity are unbound (`identity_id` set to `NULL`). The identity JSON file is deleted from disk.
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every DELETE records a full snapshot of the identity and its unbound faces. See [`14_identity_history.md`](14_identity_history.md#4-delete-history--undoredo) for:
|
||||
|
||||
- Undo via `POST /api/v1/identity/:identity_uuid/undo` — recreates identity and re-binds faces
|
||||
- Redo via `POST /api/v1/identity/:identity_uuid/redo` — re-deletes the identity
|
||||
|
||||
**Note**: Delete undo/redo reuses the same endpoints as PATCH undo/redo. The endpoint automatically detects whether the identity was deleted (undo) or needs to be re-deleted (redo) based on the history record.
|
||||
|
||||
---
|
||||
|
||||
@@ -129,124 +138,75 @@ curl -s -X PATCH "$API/api/v1/identity/$IDENTITY_UUID" \
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No fields to update or invalid UUID format |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every bind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert a bind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone bind
|
||||
- `GET /api/v1/identity/:identity_uuid/bind/history` — Query bind operations
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/files`
|
||||
## Metadata (Embedded JSON)
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
The `identities.metadata` column is a **JSONB** field that stores arbitrary structured data alongside the identity's core fields (name, status, identity_type). No schema is enforced — any valid JSON object is accepted.
|
||||
|
||||
Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range.
|
||||
### Merge Behavior
|
||||
|
||||
#### Example
|
||||
| Operation | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| **PATCH** | Shallow top-level merge: `COALESCE(metadata,'{}'::jsonb) \|\| $1::jsonb` | Sending `{"tmdb_rating": 8.5}` only adds/overwrites `tmdb_rating`; all other existing keys are preserved. |
|
||||
| **mergeinto** | Recursive deep merge — nested sub-keys are merged individually, not replaced wholesale | Target has `{"tmdb": {"biography": "..."}}`, source has `{"tmdb": {"birthday": "1904-01-18"}}` → result is `{"tmdb": {"biography": "...", "birthday": "1904-01-18"}}`. |
|
||||
| **Upload (`POST`)** | Direct overwrite — the entire `metadata` field is replaced with the request value. | |
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/files" -H "X-API-Key: $KEY"
|
||||
```
|
||||
### Validation
|
||||
|
||||
---
|
||||
| Scenario | Result |
|
||||
|----------|--------|
|
||||
| PATCH with non-object metadata (`string`, `array`, `number`, `null`) | `400 Bad Request: "metadata must be a JSON object"` |
|
||||
| mergeinto with non-object metadata | Accepted (mergeinto validates at application level) |
|
||||
| Upload with non-object metadata | Accepted (upload replaces directly) |
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/faces`
|
||||
### Conventional Keys
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
| Key | Type | Writer | Purpose |
|
||||
|-----|------|--------|---------|
|
||||
| `aliases` | `[{locale, name}]` | PATCH, mergeinto | Multilingual display names (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| `merged_into` | `{uuid, at}` | mergeinto | Marks an identity as merged (undo mechanism reads this) |
|
||||
| `tmdb_*` | various | TMDb probe | Movie metadata (biography, birthday, known_for, etc.). Written only when `MOMENTRY_TMDB_PROBE_ENABLED=true`. |
|
||||
| `source` | string | mergeinto | Tagged on aliases/metadata when added by merge (`"merge"` value) |
|
||||
|
||||
Get all face detection records associated with this identity.
|
||||
Custom keys are fully supported — no registration required.
|
||||
|
||||
#### Example
|
||||
### Search Coverage
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/faces" -H "X-API-Key: $KEY"
|
||||
```
|
||||
The identity search endpoint (`GET /api/v1/identity/search`) matches across three scopes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File where face was detected |
|
||||
| `frame_number` | integer | Frame number of detection |
|
||||
| `face_id` | string | Face ID (format: `face_{frame_number}`) |
|
||||
| `confidence` | float | Detection confidence |
|
||||
1. `i.name` — exact and ILIKE against display name
|
||||
2. `jsonb_array_elements(i.metadata->'aliases')->>'name'` — locale-tagged alias names
|
||||
3. `i.metadata::text ILIKE $1` — raw string search across the entire JSON blob (all keys, all values)
|
||||
|
||||
---
|
||||
This means searching for `"1904-01-18"` or `"biography"` will match identities whose metadata contains those strings anywhere.
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/chunks`
|
||||
### History Snapshots
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
Every `identity_history` record captures the **full metadata** in both `before_snapshot` and `after_snapshot` (as part of the complete identity JSONB dump). Undo restores the identity row — including metadata — to the `before_snapshot` state.
|
||||
|
||||
Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said.
|
||||
For merge operations, the MongoDB merge history records `metadata_fields_added` and `metadata_fields_added_paths` (dot-separated paths like `"tmdb.biography"`). Merge undo removes only those specific paths, preserving subsequent manual edits to other metadata keys.
|
||||
|
||||
#### Example
|
||||
### Best Practices
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/chunks" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "bd80fec92b0b6963d177a2c55bf713e2",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "sentence",
|
||||
"start_frame": 5103,
|
||||
"end_frame": 5127,
|
||||
"fps": 24.0,
|
||||
"start_time": 212.64,
|
||||
"end_time": 213.64,
|
||||
"text_content": "[213s-214s] Cary Grant: \"Olá!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File identifier |
|
||||
| `chunk_id` | string | Sentence chunk identifier |
|
||||
| `start_frame` | integer | Frame-accurate start position |
|
||||
| `end_frame` | integer | Frame-accurate end position |
|
||||
| `fps` | float | Frames per second |
|
||||
| `start_time` | float | Start time in seconds |
|
||||
| `end_time` | float | End time in seconds |
|
||||
| `text_content` | string | Spoken text content |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/bind`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Bind a face detection to an identity. Associates the face trace with the identity for future search and recognition.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File where face is detected |
|
||||
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
|
||||
|
||||
#### Side Effects
|
||||
|
||||
- 清除該 face detection row 的 `stranger_id`(設為 NULL)
|
||||
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "face_id": "1_5"}'
|
||||
```
|
||||
| Guideline | Reason |
|
||||
|-----------|--------|
|
||||
| Deep nesting is allowed in metadata | All metadata merge operations use `jsonb_deep_merge()` — nested sub-keys are merged recursively, not replaced wholesale |
|
||||
| Use `aliases` for display names | Frontend has built-in locale fallback logic (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| Avoid >1MB per identity | Metadata is included in search indexing (`metadata::text ILIKE`); large blobs degrade query performance |
|
||||
| Don't rely on metadata ordering | JSONB preserves insertion order but PostgreSQL does not guarantee it across operations |
|
||||
| No LLM/Gemma4 agent writes to metadata | Only API endpoints (PATCH, mergeinto, upload) and TMDb probe modify `identities.metadata` |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +255,10 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Trace bind operations share the same history/undo/redo system as single-face binds. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for endpoints.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/traces`
|
||||
@@ -382,6 +346,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
- 被 unbind 的 face 不會自動成為 stranger
|
||||
- 要重新標記為 stranger 需重新跑 Agent API(`identity/analyze`)
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Unbind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert an unbind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone unbind
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/mergeinto`
|
||||
@@ -391,6 +362,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
|
||||
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
|
||||
|
||||
#### Two Merge Cases
|
||||
|
||||
| Case | Description | Undo/Redo Support |
|
||||
|------|-------------|-------------------|
|
||||
| **stranger → identity** | Merge an auto-generated stranger identity into a known identity (TMDb or user-defined) | ✅ 24hr undo/redo |
|
||||
| **identity A → identity B** | Merge two known identities (e.g., duplicate entries) | ✅ 24hr undo/redo |
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
@@ -402,8 +380,12 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
|
||||
- 轉移所有 `face_detections.identity_id` 到目標 identity
|
||||
- 同時清除所有被轉移 rows 的 `stranger_id`
|
||||
- 將 source name 加入 target aliases (with `source: "merge"` tag)
|
||||
- 將 source aliases 加入 target aliases (if not already present)
|
||||
- 將 source metadata fields 加入 target metadata (if not already present)
|
||||
- `keep_history: true`(預設):source identity 設為 `status='merged'`,保留記錄
|
||||
- `keep_history: false`:**刪除** source identity 及其 identity JSON 檔案
|
||||
- **記錄 merge history 到 MongoDB**(支援 undo/redo)
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -411,7 +393,7 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": true}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
@@ -419,11 +401,23 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
|
||||
"data": { "faces_transferred": 52 }
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, history kept)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_added": 1,
|
||||
"metadata_fields_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | Unique merge operation ID (for undo) |
|
||||
| `faces_transferred` | integer | Number of face detections transferred |
|
||||
| `aliases_added` | integer | Number of aliases added to target |
|
||||
| `metadata_fields_added` | integer | Number of metadata fields added to target |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
@@ -433,25 +427,189 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Search identities by name (ILIKE search). Returns matching identity records.
|
||||
Undo a merge operation within 24 hours. Restores the source identity and reverts face bindings.
|
||||
|
||||
#### Undo Behavior
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Restore source identity | If `keep_history=true`: restore status to `confirmed`<br>If `keep_history=false`: recreate identity from MongoDB snapshot |
|
||||
| Restore faces | Transfer faces back to source identity |
|
||||
| Remove aliases from target | Remove aliases with `source: "merge"` tag |
|
||||
| Remove metadata fields from target | Remove fields that were added from source |
|
||||
| **Preserve manual changes** | Keep aliases/metadata manually added after merge |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Cary" -H "X-API-Key: $KEY"
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/undo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Undo merge completed: 'stranger_13894' restored, 52 faces reverted",
|
||||
"data": {
|
||||
"source_identity_restored": {
|
||||
"uuid": "a9a90105...",
|
||||
"name": "stranger_13894",
|
||||
"status": "confirmed"
|
||||
},
|
||||
"faces_reverted": 52,
|
||||
"aliases_removed_from_target": 1,
|
||||
"metadata_fields_removed_from_target": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Undo deadline expired (>24hr) or already undone |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation. See [`14_identity_history.md`](14_identity_history.md#post-apiv1identitymergemerge_idredo) for full details.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/merge/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query merge history records from MongoDB.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `source_uuid` | string | No | — | Filter by source identity UUID |
|
||||
| `target_uuid` | string | No | — | Filter by target identity UUID |
|
||||
| `merge_id` | string | No | — | Filter by specific merge ID |
|
||||
| `undone` | bool | No | — | Filter by undone status |
|
||||
| `page` | int | No | 1 | Page number |
|
||||
| `page_size` | int | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/merge/history?page=1&page_size=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"results": [
|
||||
{
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_name": "stranger_13894",
|
||||
"target_name": "Louis Viret",
|
||||
"faces_transferred": 52,
|
||||
"merged_at": "2026-05-27T10:00:00Z",
|
||||
"undo_deadline": "2026-05-28T10:00:00Z",
|
||||
"undone": false,
|
||||
"undo_expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Identity name |
|
||||
| `source` | string | Identity source |
|
||||
| `tmdb_id` | integer | TMDb ID (if source = tmdb) |
|
||||
| `file_uuid` | string | Associated file |
|
||||
| `merge_id` | string | Unique merge operation ID |
|
||||
| `source_name` | string | Source identity name |
|
||||
| `target_name` | string | Target identity name |
|
||||
| `faces_transferred` | integer | Number of faces transferred |
|
||||
| `merged_at` | datetime | When merge occurred |
|
||||
| `undo_deadline` | datetime | 24hr deadline for undo |
|
||||
| `undone` | bool | Whether merge was undone |
|
||||
| `undo_expired` | bool | Whether undo deadline passed |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search identity name → find associated chunks. Searches identity name and aliases, returns identities with their associated text chunks.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match on name and aliases) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey&file_uuid=$FILE_UUID" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"identity_id": 9,
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "tmdb",
|
||||
"tmdb_id": 1932,
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"trace_id": 41,
|
||||
"chunk_id": "llm_parent_..._204_207",
|
||||
"start_time": 204.162,
|
||||
"text_content": "...confrontation..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].identity_id` | integer | Identity ID |
|
||||
| `results[].name` | string | Identity name |
|
||||
| `results[].source` | string | Identity source (`tmdb`, `user_defined`, etc.) |
|
||||
| `results[].tmdb_id` | integer | TMDb person ID (if source = tmdb) |
|
||||
| `results[].file_uuid` | string | File where identity appears |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
| `results[].chunk_id` | string | Associated chunk ID |
|
||||
| `results[].start_time` | float | Chunk start time |
|
||||
| `results[].text_content` | string | Chunk text content |
|
||||
|
||||
---
|
||||
|
||||
@@ -628,4 +786,4 @@ PATCH /api/v1/identity/:identity_uuid
|
||||
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
|
||||
|
||||
---
|
||||
*Updated: 2026-05-25
|
||||
*Updated: 2026-05-25 — Added `GET /api/v1/file/:file_uuid/faces` with 4 binding states, filters, strangers table split
|
||||
|
||||
696
docs_v1.0/API_WORKSPACE/modules/14_identity_history.md
Normal file
696
docs_v1.0/API_WORKSPACE/modules/14_identity_history.md
Normal file
@@ -0,0 +1,696 @@
|
||||
<!-- module: identity_history -->
|
||||
<!-- description: Identity operation history, undo, and redo (PATCH, bind, unbind, bind_trace, mergeinto) -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
## Identity Operation History
|
||||
|
||||
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.
|
||||
|
||||
Three independent undo/redo systems exist:
|
||||
|
||||
| System | Storage | Operations Covered |
|
||||
|--------|---------|-------------------|
|
||||
| **PATCH** | PostgreSQL `identity_history` | `update` |
|
||||
| **Bind** | PostgreSQL `identity_history` | `bind`, `unbind`, `bind_trace` |
|
||||
| **Merge** | MongoDB `identity_merge_history` | mergeinto |
|
||||
| **Delete** | PostgreSQL `identity_history` | `delete` |
|
||||
|
||||
---
|
||||
|
||||
### 1. PATCH History & Undo/Redo
|
||||
|
||||
#### Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | Full identity record (all fields) before and after each PATCH |
|
||||
| Max records | 256 per identity (oldest auto-deleted when limit exceeded) |
|
||||
| Undo steps | Unlimited (no expiry, no step limit) |
|
||||
| Redo stack | Cleared on new PATCH (`is_undone=true` + `operation='update'` records are deleted) |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
PATCH 1 → PATCH 2 (undo stack)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
|
||||
```
|
||||
|
||||
A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent PATCH operations. Restores the identity's `before_snapshot` and marks the history records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply (max records undone in one call) |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores `name`, `identity_type`, `source`, `status`, `metadata`, `tmdb_id`, `tmdb_profile` from the last record's `before_snapshot`
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"undone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `current_state` | object | Full identity state after undo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone PATCH operations. Restores the identity's `after_snapshot` and marks the history records as no longer undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores all identity fields from the last record's `after_snapshot`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"redone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "John Smith",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": { "aliases": [...] },
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `current_state` | object | Full identity state after redo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to `operation='update'`).
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 5,
|
||||
"undo_stack_count": 3,
|
||||
"redo_stack_count": 2,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 42,
|
||||
"operation": "update",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 41,
|
||||
"operation": "update",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T11:30:00Z",
|
||||
"undone_at": "2026-05-27T13:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total PATCH history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`"update"` for PATCH) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the PATCH was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 2. Bind/Unbind/Trace History & Undo/Redo
|
||||
|
||||
All three operations (`bind`, `unbind`, `bind_trace`) share a single history table and undo/redo stack.
|
||||
|
||||
#### Bind Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table (same table as PATCH) |
|
||||
| Snapshot | `{"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}` |
|
||||
| Max records | 256 per identity (shared limit across all operation types) |
|
||||
| Undo steps | Unlimited (`steps` param) |
|
||||
| Redo stack | Cleared on new bind/unbind/bind_trace (`operation IN ('bind','unbind','bind_trace')` + `is_undone=true` records deleted) |
|
||||
| Stack isolation | Bind redo stack is **independent** from PATCH redo stack — clearing one does not affect the other |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
A new bind/unbind/trace after undo clears only the bind redo stack (operations with `IN ('bind','unbind','bind_trace')`).
|
||||
|
||||
##### Snapshot Format
|
||||
|
||||
**Before (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": null
|
||||
}
|
||||
```
|
||||
|
||||
**After (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": 9
|
||||
}
|
||||
```
|
||||
|
||||
**Before (unbind) — binding existed before:**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": 9
|
||||
}
|
||||
```
|
||||
|
||||
**After (unbind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": null
|
||||
}
|
||||
```
|
||||
|
||||
For `bind_trace`, the snapshot uses `trace_id` instead of `face_id`, with `identity_id_before` capturing the first face's identity in that trace.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent bind/unbind/bind_trace operations. Restores `identity_id_before` from the snapshot and marks records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_before` — for bind this is `null` (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "bind",
|
||||
"undone_count": 1,
|
||||
"affected_rows": 53
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation undone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone bind/unbind/bind_trace operations. Restores `identity_id_after` from the snapshot.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_after` — for bind this is the identity the face was bound to, for unbind this is `null`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "unbind",
|
||||
"redone_count": 1,
|
||||
"affected_rows": 1
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation redone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/bind/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 3,
|
||||
"undo_stack_count": 2,
|
||||
"redo_stack_count": 1,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 52,
|
||||
"operation": "bind_trace",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T14:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 51,
|
||||
"operation": "unbind",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T13:00:00Z",
|
||||
"undone_at": "2026-05-27T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"history_id": 50,
|
||||
"operation": "bind",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total bind history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the operation was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/bind/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 3. Merge History & Undo/Redo
|
||||
|
||||
Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.
|
||||
|
||||
#### Merge Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | MongoDB `identity_merge_history` collection |
|
||||
| Snapshot | Full source identity state + target identity state + aliases/metadata diffs |
|
||||
| Trigger | Every mergeinto with `keep_history=true` |
|
||||
| Undo deadline | 24 hours (renewed on redo) |
|
||||
| Redo support | Yes — restores undone merges with new 24hr deadline |
|
||||
| Max records | Unlimited |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
Already documented in [`07_identity.md`](07_identity.md#post-apiv1identitymergemerge_idundo). See that document for full details.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation within the renewed 24-hour deadline.
|
||||
|
||||
##### Request
|
||||
|
||||
No body required. The merge ID is taken from the URL path.
|
||||
|
||||
##### Behavior
|
||||
|
||||
1. Validates the merge record exists and `undone=true` (not already active)
|
||||
2. Checks the 24-hour undo deadline (if expired, the redo is rejected)
|
||||
3. Restores face bindings: moves all faces from `target_identity` back to `source_identity`
|
||||
4. Re-adds aliases that were removed by the undo (aliases with `source: "merge"` tag)
|
||||
5. Re-adds metadata fields that were removed by the undo
|
||||
6. If `keep_history=true`: sets `source_identity.status = 'merged'` again
|
||||
7. If `keep_history=false`: recreates source identity from the `undone_snapshot` stored at undo time
|
||||
8. Syncs both identity JSON files to disk
|
||||
9. Sets `undone=false`, clears `undone_snapshot`, renews `undo_deadline = NOW() + 24h`
|
||||
10. Records `redone_by` user for audit
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Redo merge completed: merged 'stranger_13894' into 'Louis Viret' (52 faces transferred)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_re_added": 1,
|
||||
"metadata_fields_re_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | The merge operation ID |
|
||||
| `faces_transferred` | integer | Number of faces transferred from source to target |
|
||||
| `aliases_re_added` | integer | Number of aliases restored to target |
|
||||
| `metadata_fields_re_added` | integer | Number of metadata fields restored to target |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Merge not undone, deadline expired, or cannot redo |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 4. Delete History & Undo/Redo
|
||||
|
||||
#### Delete Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | `{"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}` |
|
||||
| Max records | 1 active delete record per identity (redo stack cleared on new delete) |
|
||||
| Undo support | Yes — recreates identity row, re-binds faces |
|
||||
| Redo support | Yes — re-deletes the identity |
|
||||
| Identity file | Deleted on delete, recreated on undo |
|
||||
|
||||
#### Snapshot Format
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"id": 9,
|
||||
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
},
|
||||
"unbound_faces": [
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"trace_id": null
|
||||
},
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_6",
|
||||
"trace_id": 906
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Stack Model
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
A new delete after an undo clears the delete redo stack (no redo possible for the old delete).
|
||||
|
||||
#### Undo Behavior (via existing `POST /api/v1/identity/:identity_uuid/undo`)
|
||||
|
||||
1. Normal identity lookup fails (row was deleted)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=false` matching the UUID in the snapshot
|
||||
3. Recreates the identity row (new internal `id`, same UUID)
|
||||
4. Re-binds all faces listed in `unbound_faces` to the new identity
|
||||
5. Deletes the `identity_history` delete record as `is_undone=true` with `undone_at=NOW()`
|
||||
6. Syncs `identity.json` to disk
|
||||
7. Updates `_index.json`
|
||||
|
||||
#### Redo Behavior (via existing `POST /api/v1/identity/:identity_uuid/redo`)
|
||||
|
||||
1. Identity lookup succeeds (identity was restored by prior undo)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=true` matching the identity_id
|
||||
3. Deletes `identity.json` from disk
|
||||
4. Unbinds all faces (`identity_id = NULL`)
|
||||
5. Deletes the identity row
|
||||
6. Marks the delete history record as `is_undone=false`
|
||||
7. Returns success
|
||||
|
||||
#### Error Responses (delete undo/redo)
|
||||
|
||||
| HTTP | Scenario |
|
||||
|------|----------|
|
||||
| `400` | No delete history available (either no delete or already undone/redone) |
|
||||
| `404` | Identity not found (for redo — identity wasn't restored) |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo
|
||||
|
||||
| Aspect | PATCH Undo/Redo | Bind Undo/Redo | Merge Undo/Redo | Delete Undo/Redo |
|
||||
|--------|----------------|----------------|-----------------|------------------|
|
||||
| Storage | PostgreSQL `identity_history` | PostgreSQL `identity_history` | MongoDB `identity_merge_history` | PostgreSQL `identity_history` |
|
||||
| Operation filter | `operation='update'` | `operation IN ('bind','unbind','bind_trace')` | — | `operation='delete'` |
|
||||
| Trigger | Every PATCH | Every bind/unbind/bind_trace | Every mergeinto with `keep_history=true` | Every DELETE |
|
||||
| Undo deadline | None (unlimited) | None (unlimited) | 24 hours (renewed on redo) | None (unlimited) |
|
||||
| Redo support | Yes | Yes | Yes | Yes |
|
||||
| Step undo | Yes (`steps` param) | Yes (`steps` param) | No (full undo/redo only) | No (single record) |
|
||||
| Max records | 256 per identity | 256 per identity (shared) | Unlimited | 256 per identity (shared) |
|
||||
| User tracking | `user_id` + `user_source` | `user_id` + `user_source` | `performed_by_user` + `undone_by` / `redone_by` | `user_id` + `user_source` |
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-28*
|
||||
36
docs_v1.0/API_WORKSPACE/narratives/marcom_intro.md
Normal file
36
docs_v1.0/API_WORKSPACE/narratives/marcom_intro.md
Normal file
@@ -0,0 +1,36 @@
|
||||
<!-- narrative: marcom_intro -->
|
||||
<!-- description: Intro section for Marcom training manual -->
|
||||
<!-- depends: -->
|
||||
|
||||
## About This Manual
|
||||
|
||||
This training manual is designed for the Marcom team to understand and use the Momentry Core API.
|
||||
|
||||
### Demo Credentials
|
||||
|
||||
**API Key**: `muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69`
|
||||
|
||||
**SFTPGo** (for video upload):
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| SFTP Host | `sftpgo.momentry.ddns.net` |
|
||||
| SFTP Port | `2022` |
|
||||
| Username | `demo` |
|
||||
| Password | `demopassword123` |
|
||||
| Web UI | `https://sftpgo.momentry.ddns.net` |
|
||||
|
||||
### Quick Examples
|
||||
|
||||
**List all videos:**
|
||||
```bash
|
||||
curl -s -H "X-API-Key: $KEY" "$API/api/v1/files/scan"
|
||||
```
|
||||
|
||||
**Search:**
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"query": "example", "limit": 5}'
|
||||
```
|
||||
588
docs_v1.0/DESIGN/ASRX_HYBRID_PIPELINE_V1.0.md
Normal file
588
docs_v1.0/DESIGN/ASRX_HYBRID_PIPELINE_V1.0.md
Normal file
@@ -0,0 +1,588 @@
|
||||
# ASRX Hybrid Pipeline v1.0 — 聲紋分離混合架構
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **範圍** | ASRX 處理器重構:whisperx → VAD-first hybrid pipeline |
|
||||
| **狀態** | Draft |
|
||||
| **適用版本** | Momentry Core V4.0+ |
|
||||
| **作者** | OpenCode / Warren |
|
||||
| **建立日期** | 2026-06-01 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 問題
|
||||
|
||||
### 1.1 現有問題
|
||||
|
||||
| 問題 | 說明 | 影響 |
|
||||
|------|------|------|
|
||||
| **Whisper 合併短句** | `whisper small` 會將兩個人的對話錯認成一個連續段 (A+B → 一句) | ASR segment 內混兩人話語,speaker 無法分離 |
|
||||
| **ASRX v2 speaker_id = null** | `asrx_processor_v2.py` 使用 `whisperx.DiarizationPipeline()` 但該 API 未在 whisperx `__init__.py` 暴露 | 所有 segment speaker 均為 null |
|
||||
| **文字丟失** | `asrx_processor_custom.py` 的 `SelfASRXFixed.process_with_segments()` 只輸出 `text: ""` | Rule 1 合併時無文字可用 |
|
||||
| **錯誤的聲紋後端** | `asrx_processor_v2.py` 依賴 whisperx 內建 diarization,但該功能不穩定 | 準確度 ~85%,需 HF token |
|
||||
| **多版本混亂** | 7 個 root-level 變體、14 個 asrx_self 檔案,生產環境使用錯誤版本 | 維護困難,不知哪個是對的 |
|
||||
|
||||
### 1.2 痛點場景
|
||||
|
||||
**兩個說話人短句來回切換**(訪談、對話):
|
||||
|
||||
```
|
||||
Audio: A(2s) → B(1.5s) → A(3s)
|
||||
Whisper: ───────[0-7s, "A+B+A 全部混在一起"]───────
|
||||
```
|
||||
|
||||
Whisper 在句間停頓處不切段,導致 ASR 時間邊界無法反映 speaker 切換。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架構
|
||||
|
||||
### 2.1 核心原則
|
||||
|
||||
1. **VAD 先定邊界** — 用 VAD 在句間停頓處切段,取代 whisper 的邊界
|
||||
2. **ASR 後做** — 每段各自轉錄,保有獨立文字
|
||||
3. **聲紋聚類定 speaker** — ECAPA-TDNN + AgglomerativeClustering
|
||||
|
||||
### 2.2 5 步 Pipeline
|
||||
|
||||
```
|
||||
Audio
|
||||
│
|
||||
① whisper (一次, 粗略定位)
|
||||
│ 找到說話段 + 初步文字 + 語種
|
||||
│ [0-7s, "今天天氣很好我覺得也不錯對啊", zh]
|
||||
│
|
||||
② VAD scan (在每段內細切)
|
||||
│ 利用句間停頓切開
|
||||
│ 段1 [0-2s] 段2 [2-3.5s] 段3 [3.5-7s]
|
||||
│
|
||||
③ whisper per refined segment (各段轉錄)
|
||||
│ 段1 → "今天天氣很好" (zh, 0.98)
|
||||
│ 段2 → "我覺得也不錯" (zh, 0.97)
|
||||
│ 段3 → "對啊" (zh, 0.96)
|
||||
│
|
||||
④ ECAPA-TDNN per refined segment (聲紋提取)
|
||||
│ 段1 → emb[0] (192-dim)
|
||||
│ 段2 → emb[1] (192-dim)
|
||||
│ 段3 → emb[2] (192-dim)
|
||||
│
|
||||
⑤ AgglomerativeClustering (聚類定 speaker)
|
||||
│ emb[0]=SPEAKER_0, emb[1]=SPEAKER_1, emb[2]=SPEAKER_0
|
||||
│
|
||||
輸出:
|
||||
start end text language speaker_id
|
||||
0.0 2.0 今天天氣很好 zh SPEAKER_0
|
||||
2.0 3.5 我覺得也不錯 zh SPEAKER_1
|
||||
3.5 7.0 對啊 zh SPEAKER_0
|
||||
```
|
||||
|
||||
### 2.3 流程圖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ asrx_processor.py │
|
||||
│ (wrapper) │
|
||||
│ │
|
||||
│ ① ffprobe → select best track → ffmpeg → 16kHz WAV │
|
||||
│ │
|
||||
│ ② SelfASRXFixed.process(audio_wav, file_uuid) │
|
||||
│ │ │
|
||||
│ ├─ Step 1: whisper.transcribe() → rough segments │
|
||||
│ ├─ Step 2: VAD scan each rough segment │
|
||||
│ ├─ Step 3: whisper per refined segment → text+language │
|
||||
│ ├─ Step 4: ECAPA-TDNN per segment → 192-dim embedding │
|
||||
│ ├─ Step 5: AgglomerativeClustering → speaker_labels │
|
||||
│ │ │
|
||||
│ ├─ Step 6: Store embeddings in Qdrant │
|
||||
│ │ └─ {file_uuid, speaker_id, text, language, start, end} │
|
||||
│ │ │
|
||||
│ └─ Step 7: Classify high-quality embeddings │
|
||||
│ ├─ quality > threshold → reference profile │
|
||||
│ ├─ 送入聲音分類模型推論性別/屬性 │
|
||||
│ └─ 寫入 Qdrant (type: speaker_reference) │
|
||||
│ │
|
||||
│ ③ 輸出 JSON 格式 (不含 embedding) │
|
||||
│ │
|
||||
│ Rust: rule1_ingest.rs │
|
||||
│ └─ pre_chunks(processor_type='asrx') → chunks │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 檔案組織
|
||||
|
||||
### 3.1 最終檔案結構
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── asrx_processor.py ← production (cleaned custom.py)
|
||||
│
|
||||
└── asrx_self/ ← 核心庫
|
||||
├── __init__.py ← package marker
|
||||
├── vad.py ← Silero VAD (新增 scan_within_segment)
|
||||
├── whisper_local.py ← 🆕 封裝 whisper 載入+轉錄
|
||||
├── speaker_encoder.py ← ECAPA-TDNN 192-dim
|
||||
├── speaker_cluster_fixed.py ← AgglomerativeClustering
|
||||
└── main_fixed.py ← 🔧 重寫為 5 步 pipeline
|
||||
```
|
||||
|
||||
### 3.2 刪除清單
|
||||
|
||||
**Root-level 變體**(全部刪除):
|
||||
|
||||
| 檔案 | 原因 |
|
||||
|------|------|
|
||||
| `asrx_processor.py` | 原始 whisperx 版,diarization 壞的 |
|
||||
| `asrx_processor_v2.py` | 同上,Rust 目前錯誤呼叫此檔 |
|
||||
| `asrx_processor_v2_noalign.py` | 跳過對齊但 diarization 仍壞 |
|
||||
| `asrx_processor_v2_transcribe.py` | 只轉錄不做 speaker |
|
||||
| `asrx_processor_simplified.py` | 變體 |
|
||||
| `asrx_processor_contract_v1.py` | 18KB,pyannote,需 HF token |
|
||||
|
||||
**asrx_self 內被取代的舊版**:
|
||||
|
||||
| 檔案 | 原因 | 取代者 |
|
||||
|------|------|--------|
|
||||
| `main.py` | 用 SpectralClustering,有 NaN 問題 | `main_fixed.py` |
|
||||
| `speaker_cluster.py` | 用 SpectralClustering,不穩定 | `speaker_cluster_fixed.py` |
|
||||
|
||||
### 3.3 搬離清單
|
||||
|
||||
非生產工具搬至 `tools/asrx/`:
|
||||
|
||||
```
|
||||
tools/asrx/
|
||||
├── integrate_face_asrx_speaker.py
|
||||
├── speaker_player_gui.py
|
||||
├── speaker_player_gui_face.py
|
||||
├── speaker_player_interactive.py
|
||||
├── speaker_audio_player.py
|
||||
├── test_long_movie.py
|
||||
├── test_gui_face_player.py
|
||||
└── docs/
|
||||
├── FINAL_TEST_REPORT.md
|
||||
├── GUI_FACE_PLAYER_USAGE.md
|
||||
├── LONG_MOVIE_TEST_SUMMARY.md
|
||||
└── SPEAKER_PLAYER_GUIDE.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 4. Qdrant 聲紋向量儲存
|
||||
|
||||
### 4.1 儲存流程
|
||||
|
||||
```
|
||||
Step 4 輸出: 每個 refined segment 有 {embedding: [192-dim], text, language, start, end}
|
||||
Step 5 輸出: 每個 segment 被標上 speaker_id {SPEAKER_0, SPEAKER_1, ...}
|
||||
|
||||
Step 6: Qdrant 儲存
|
||||
┌─ 每個 segment → Qdrant point
|
||||
│ point_id = hash(file_uuid + segment_index) ← 可重複查詢
|
||||
│ vector = embedding (192-dim)
|
||||
│ payload = {
|
||||
│ "file_uuid": str, ← 聚類後填入
|
||||
│ "speaker_id": str, ← 聚類後填入
|
||||
│ "text": str, ← ASR 轉錄結果
|
||||
│ "language": str, ← 語種 (zh/en/...)
|
||||
│ "start_time": f64, ← 秒
|
||||
│ "end_time": f64, ← 秒
|
||||
│ "type": "speaker_embedding" ← 便於區分
|
||||
│ }
|
||||
└─
|
||||
```
|
||||
|
||||
### 4.2 Qdrant Collection
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| Collection Name | `momentry_speaker` (或共用現有 collection) |
|
||||
| Vector Dimension | 192 (ECAPA-TDNN 輸出) |
|
||||
| Distance Metric | Cosine |
|
||||
| Point ID | `hash(file_uuid + "_" + segment_index)` |
|
||||
|
||||
### 4.3 Rust `upsert_speaker_embedding`
|
||||
|
||||
```rust
|
||||
impl QdrantDb {
|
||||
pub async fn upsert_speaker_embedding(
|
||||
&self,
|
||||
point_id: u64,
|
||||
vector: &[f32],
|
||||
file_uuid: &str,
|
||||
speaker_id: &str,
|
||||
text: &str,
|
||||
language: &str,
|
||||
start_time: f64,
|
||||
end_time: f64,
|
||||
) -> Result<()> {
|
||||
// Qdrant PUT /collections/{collection}/points?wait=true
|
||||
// payload: {file_uuid, speaker_id, text, language, start_time, end_time, type: "speaker_embedding"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 與現有 Face Embedding 的關係
|
||||
|
||||
| 類別 | Qdrant Collection | Dim | Payload |
|
||||
|------|-------------------|-----|---------|
|
||||
| Face | `momentry` (self.collection_name) | 512 (FaceNet) | `file_uuid, trace_id, frame_number` |
|
||||
| **Speaker** | `momentry` 或獨立 collection | **192** (ECAPA-TDNN) | `file_uuid, speaker_id, text, language, start, end` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 模組詳細設計
|
||||
|
||||
### 5.1 `vad.py` — 語音活動檢測
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | Silero VAD (torch.hub, snakers4/silero-vad) |
|
||||
| 現有函數 | `load_vad_model()`, `extract_speech_segments()` |
|
||||
| **新增函數** | **`scan_within_segment(wav, start_sec, end_sec, model, utils, min_speech_duration_ms=500)`** |
|
||||
|
||||
`scan_within_segment` 作用:
|
||||
- 在一個時間範圍 `[start_sec, end_sec]` 內執行 VAD 掃描
|
||||
- 只回傳該範圍內的語音子片段 `[(s1, e1), (s2, e2), ...]`
|
||||
- 利用句間停頓切分,解決 whisper 合併問題
|
||||
|
||||
### 5.2 `whisper_local.py` 🆕 — Whisper 封裝
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | `whisper.load_model("base")` (可設定) |
|
||||
| 函數 | `load_model()`, `transcribe_segment(audio, start, end)` |
|
||||
|
||||
```python
|
||||
def transcribe_segment(wav, sample_rate, start_sec, end_sec, model) -> dict:
|
||||
"""轉錄單一段落,回傳 {text, language, lang_prob, segments}"""
|
||||
```
|
||||
|
||||
每段獨立轉錄,保留語言與信心度。
|
||||
|
||||
### 5.3 `speaker_encoder.py` — 聲紋編碼器
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | SpeechBrain ECAPA-TDNN (`spkrec-ecapa-voxceleb`) |
|
||||
| 輸出維度 | 192-dim |
|
||||
| EER | 0.80% (VoxCeleb1) |
|
||||
| 授權 | MIT (不需要 HuggingFace token) |
|
||||
| 函數 | `load_speaker_encoder()`, `extract_speaker_embedding()`, `extract_speaker_embeddings_batch()` |
|
||||
|
||||
### 5.4 `speaker_cluster_fixed.py` — 說話人聚類
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 演算法 | AgglomerativeClustering (cosine + average linkage) |
|
||||
| 取代 | `speaker_cluster.py` (SpectralClustering, NaN 問題) |
|
||||
| 函數 | `robust_speaker_clustering(embeddings, n_speakers=None, max_speakers=10)` |
|
||||
|
||||
### 5.5 `main_fixed.py` 🔧 — 核心調度器(7 步 Pipeline)
|
||||
|
||||
```python
|
||||
class SelfASRXFixed:
|
||||
def process(self, audio_path, output_path=None, file_uuid=None):
|
||||
"""
|
||||
7 步 speaker diarization pipeline
|
||||
|
||||
Steps:
|
||||
1. whisper.transcribe(audio) → rough segments + text + language
|
||||
2. VAD scan each rough segment → refined segments
|
||||
3. whisper per refined segment → {text, language, lang_prob}
|
||||
4. ECAPA-TDNN per refined segment → 192-dim embeddings
|
||||
5. AgglomerativeClustering → speaker_labels
|
||||
6. Store all embeddings in Qdrant (if file_uuid provided)
|
||||
payload: {file_uuid, speaker_id, text, language, start_time, end_time, type: "speaker_embedding"}
|
||||
7. High-quality embeddings (quality > threshold) → classify + store reference
|
||||
payload: {type: "speaker_reference", file_uuid, speaker_id, n_segments, avg_quality, ...}
|
||||
|
||||
Returns:
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": float, "end": float,
|
||||
"text": str, "language": str,
|
||||
"lang_prob": float, "speaker": str,
|
||||
"speaker_id": str, "quality": float
|
||||
},
|
||||
...
|
||||
],
|
||||
"speaker_stats": {...},
|
||||
"n_speakers": int,
|
||||
"total_duration": float,
|
||||
"references": [
|
||||
{
|
||||
"speaker_id": str,
|
||||
"n_segments": int,
|
||||
"avg_quality": float,
|
||||
"gender": str
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
def _store_speaker_embeddings(self, segments, file_uuid):
|
||||
"""Step 6: 每個 segment 的 192-dim embedding 存入 Qdrant"""
|
||||
|
||||
def _classify_high_quality_speakers(self, segments, embeddings, labels, file_uuid):
|
||||
"""Step 7: 高品質聲紋分級 + 分類 → Qdrant reference profile"""
|
||||
|
||||
**移除**:
|
||||
|
||||
| 舊方法 | 原因 |
|
||||
|--------|------|
|
||||
| `process_with_segments(audio, asr_segments)` | 外部 ASR 邊界來源不可靠,被 VAD 取代 |
|
||||
| `process()` VAD-only fallback | 無文字輸出,被完整 pipeline 取代 |
|
||||
|
||||
### 5.6 `speaker_classifier.py` 🆕 — 高品質聲紋分級與分類
|
||||
|
||||
#### 目的
|
||||
|
||||
聚類後,對每個 cluster 的 embedding 進行品質評估,高於閾值的獨立建檔,並用外部模型做自動分類。
|
||||
|
||||
#### 流程
|
||||
|
||||
```
|
||||
Step ⑤ 聚類後,每個 segment 有 {embedding, speaker_id}
|
||||
│
|
||||
└─ Compute quality score per embedding
|
||||
│
|
||||
├─ 低於閾值 → 寫入 Qdrant (一般 speaker_embedding)
|
||||
│
|
||||
└─ 高於閾值 (quality > 0.85)
|
||||
├─ 獨立建 reference profile
|
||||
└─ 送入「支持聲音的模型」做分類
|
||||
├─ 語者性別 (male/female)
|
||||
├─ 語種口音 (zh-CN / zh-TW / en-US)
|
||||
└─ 或跨影片 speaker 匹配用
|
||||
```
|
||||
|
||||
#### Quality Score 計算
|
||||
|
||||
```python
|
||||
def compute_embedding_quality(embeddings, labels, threshold=0.85):
|
||||
"""
|
||||
每個 embedding 到所屬 cluster centroid 的餘弦相似度
|
||||
|
||||
Args:
|
||||
embeddings: [n_segments, 192]
|
||||
labels: [n_segments] 聚類標籤
|
||||
threshold: 高品質門檻
|
||||
|
||||
Returns:
|
||||
qualities: [n_segments] 每個 embedding 的品質分數
|
||||
high_quality_mask: [n_segments] bool 陣列
|
||||
"""
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
|
||||
unique_labels = set(labels)
|
||||
centroids = {}
|
||||
for label in unique_labels:
|
||||
mask = labels == label
|
||||
centroid = np.mean(embeddings[mask], axis=0)
|
||||
centroid = centroid / np.linalg.norm(centroid)
|
||||
centroids[label] = centroid
|
||||
|
||||
qualities = []
|
||||
for i, (emb, label) in enumerate(zip(embeddings, labels)):
|
||||
sim = cosine_similarity([emb], [centroids[label]])[0][0]
|
||||
qualities.append(sim)
|
||||
|
||||
return np.array(qualities), np.array(qualities) >= threshold
|
||||
```
|
||||
|
||||
#### Reference Profile 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"point_id": "hash(speaker_reference_" + file_uuid + "_" + speaker_id + "_" + cluster_index)",
|
||||
"vector": "[192-dim centroid embedding]",
|
||||
"payload": {
|
||||
"type": "speaker_reference",
|
||||
"file_uuid": "來源影片",
|
||||
"speaker_id": "SPEAKER_0",
|
||||
"n_segments": 25,
|
||||
"avg_quality": 0.92,
|
||||
"total_duration": 45.3,
|
||||
"language": "zh",
|
||||
"gender": "male",
|
||||
"text_samples": ["今天天氣很好", "我覺得也不錯", "..."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 支援的聲音分類模型(選項)
|
||||
|
||||
| 模型 | 用途 | 優點 | 缺點 |
|
||||
|------|------|------|------|
|
||||
| **SpeechBrain gender classifier** | 性別分類 | 已整合 ECAPA-TDNN | 只分 male/female |
|
||||
| **CLAP** (LAION) | 零樣本音頻分類 | 可自訂 label text | 需額外安裝 |
|
||||
| **YAMNet** | 聲音事件分類 | Google 出品,521 classes | 不擅長語者屬性 |
|
||||
| **Wav2Vec2-BERT** (speechbrain) | 情感/屬性 | 多維度分類 | 模型較大 |
|
||||
| **自建 identity classifier** | 跨影片 speaker 匹配 | 與現有 identity 系統對接 | 需累積 reference data |
|
||||
|
||||
> **待決定**: 選擇哪個分類模型,由後續 POC 決定。
|
||||
|
||||
#### `main_fixed.py` 新增方法
|
||||
|
||||
```python
|
||||
class SelfASRXFixed:
|
||||
# ... 既有 6 個步驟 ...
|
||||
|
||||
def _classify_high_quality_speakers(self, segments, embeddings, labels, file_uuid):
|
||||
"""
|
||||
步驟 7: 高品質聲紋分級與分類
|
||||
|
||||
1. 計算 quality score
|
||||
2. 高於閾值者建立 reference profile
|
||||
3. 用分類模型推論性別/屬性
|
||||
4. 寫入 Qdrant (type: speaker_reference)
|
||||
"""
|
||||
qualities, mask = compute_embedding_quality(embeddings, labels)
|
||||
|
||||
for i, (seg, emb, label, quality, is_high) in enumerate(
|
||||
zip(segments, embeddings, labels, qualities, mask)
|
||||
):
|
||||
seg["quality"] = float(quality)
|
||||
if is_high:
|
||||
profile = self._build_reference_profile(
|
||||
emb, seg, file_uuid
|
||||
)
|
||||
# 分類 (placeholder)
|
||||
# gender = classify_gender(embedding)
|
||||
self._store_speaker_reference(profile)
|
||||
```
|
||||
|
||||
### 5.7 `asrx_processor.py` — 清理後的 wrapper
|
||||
|
||||
清理項目:
|
||||
|
||||
| 問題 | 位置 | 修法 |
|
||||
|------|------|------|
|
||||
| 硬編碼 UUID `dd61fda8...` | line 155 | 移除該 fallback path |
|
||||
| `os.chdir(script_dir)` | line 112 | 改區域性 Path 操作 |
|
||||
| ASR 文字丟棄 | line 258 | `text` 來自新 pipeline |
|
||||
| `_debug` dict | line 222 | 移除 |
|
||||
| `max_speakers=10` 寫死 | line 201 | 改 CLI 參數 `--max-speakers` |
|
||||
| 載入外部 ASR segments | line 148-174 | 移除(不再需要) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 輸出格式
|
||||
|
||||
### 6.1 ASRX JSON Output (由 `asrx_processor.py` 寫入)
|
||||
|
||||
> **注意**: 192-dim embedding 不在此 JSON 中。embedding 在 Python 端直接送入 Qdrant,JSON 只保留中繼資料。
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "zh",
|
||||
"segments": [
|
||||
{
|
||||
"start_time": 0.0,
|
||||
"end_time": 2.0,
|
||||
"start_frame": 0,
|
||||
"end_frame": 60,
|
||||
"text": "今天天氣很好",
|
||||
"speaker_id": "SPEAKER_0",
|
||||
"language": "zh",
|
||||
"lang_prob": 0.98
|
||||
},
|
||||
{
|
||||
"start_time": 2.0,
|
||||
"end_time": 3.5,
|
||||
"start_frame": 60,
|
||||
"end_frame": 105,
|
||||
"text": "我覺得也不錯",
|
||||
"speaker_id": "SPEAKER_1",
|
||||
"language": "zh",
|
||||
"lang_prob": 0.97
|
||||
}
|
||||
],
|
||||
"n_speakers": 2,
|
||||
"speaker_stats": {
|
||||
"SPEAKER_0": {"count": 1, "duration": 2.0},
|
||||
"SPEAKER_1": {"count": 1, "duration": 1.5}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Qdrant Point 格式 (由 Python `_store_speaker_embeddings` 寫入)
|
||||
|
||||
> Embedding 不經過 Rust,直接在 Python 端完成 Qdrant HTTP PUT。
|
||||
|
||||
| Qdrant 欄位 | 值 | 說明 |
|
||||
|-------------|-----|------|
|
||||
| `id` | `hash(file_uuid + "_" + segment_index)` | 可重複查詢的 point ID |
|
||||
| `vector` | `[f32; 192]` | ECAPA-TDNN 聲紋向量 |
|
||||
| `payload.file_uuid` | `str` | 影片識別碼 |
|
||||
| `payload.speaker_id` | `str` | 聚類後的 speaker 標籤 |
|
||||
| `payload.text` | `str` | 該段的轉錄文字 |
|
||||
| `payload.language` | `str` | 語種 (`zh`/`en`) |
|
||||
| `payload.start_time` | `f64` | 開始時間(秒) |
|
||||
| `payload.end_time` | `f64` | 結束時間(秒) |
|
||||
| `payload.type` | `"speaker_embedding"` | 便於與 face_embedding 區分 |
|
||||
|
||||
### 6.3 Rust `AsrxResult` 對應
|
||||
|
||||
```rust
|
||||
pub struct AsrxSegment {
|
||||
pub start_time: f64, // serde(alias = "start")
|
||||
pub end_time: f64, // serde(alias = "end")
|
||||
pub start_frame: u64, // default 0
|
||||
pub end_frame: u64, // default 0
|
||||
pub text: String,
|
||||
pub speaker_id: Option<String>,
|
||||
pub language: Option<String>, // 🆕 新增
|
||||
pub lang_prob: Option<f64>, // 🆕 新增
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Rust 端變動
|
||||
|
||||
| 檔案 | 變動 |
|
||||
|------|------|
|
||||
| `src/core/processor/asrx.rs` | `asrx_processor_v2.py` → `asrx_processor.py` |
|
||||
| `src/core/processor/asrx.rs` | `AsrxSegment` 新增 `language`, `lang_prob` 欄位 |
|
||||
| `src/core/processor/asrx.rs` | 傳遞 `--file-uuid` 給 Python 腳本,讓 Python 端可直接寫入 Qdrant |
|
||||
| `src/core/chunk/rule1_ingest.rs` | 若 `pre_chunks` data 含 `language` 則帶入 chunk metadata |
|
||||
| `src/core/db/qdrant_db.rs` | 🆕 新增 `upsert_speaker_embedding()` 方法 (可選,若 Python 端直接寫 Qdrant 則不需) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 遷移計畫
|
||||
|
||||
### 實作順序 (依賴關係排序)
|
||||
|
||||
| 步驟 | 內容 | 檔案 | 風險 |
|
||||
|------|------|------|------|
|
||||
| **S1** | `vad.py`: 新增 `scan_within_segment()` | `asrx_self/vad.py` | 低 |
|
||||
| **S2** | 🆕 `whisper_local.py`: 封裝 whisper 載入 + 轉錄 | `asrx_self/whisper_local.py` | 低 |
|
||||
| **S3** | 🔧 `main_fixed.py`: 重寫為 7 步 pipeline | `asrx_self/main_fixed.py` | 中 |
|
||||
| **S4** | 🆕 `speaker_classifier.py`: 性別分類器 | `asrx_self/speaker_classifier.py` | 低 |
|
||||
| **S5** | 🔧 `custom.py` cleanup + rename → `asrx_processor.py` | `asrx_processor_custom.py` | 低 |
|
||||
| **S6** | 🔧 Rust `asrx.rs`: 改指向 + 傳 `--file-uuid` | `src/core/processor/asrx.rs` | 低 |
|
||||
| **S7** | ✅ 驗證:build + playground 測試 | — | 中 |
|
||||
| **S8** | 🧹 刪除變體 + 搬離工具 | — | 低 |
|
||||
|
||||
### 驗證標準
|
||||
|
||||
1. `cargo build` 通過
|
||||
2. Playground 3003: 註冊影片 → ASRX processor 完成
|
||||
3. 輸出 JSON 中 `speaker_id` 非 `null`
|
||||
4. Qdrant collection 有 `speaker_embedding` 點
|
||||
5. 性別正確標記 (male/female)
|
||||
|
||||
---
|
||||
|
||||
## 9. 版本歷史
|
||||
|
||||
| 版本 | 日期 | 修改者 | 說明 |
|
||||
|------|------|--------|------|
|
||||
| V1.0 | 2026-06-01 | OpenCode | 初始版本:7 步 hybrid pipeline + Qdrant 聲紋儲存 + 高品質分類 |
|
||||
385
docs_v1.0/DESIGN/Modular_Doc_System_V1.0.md
Normal file
385
docs_v1.0/DESIGN/Modular_Doc_System_V1.0.md
Normal file
@@ -0,0 +1,385 @@
|
||||
---
|
||||
document_type: "design"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "模組生成式文件產出系統"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"
|
||||
tags:
|
||||
- "documentation"
|
||||
- "modular"
|
||||
- "generated-docs"
|
||||
- "workspace"
|
||||
ai_query_hints:
|
||||
- "查詢模組生成式文件產出系統的設計理念"
|
||||
- "如何使用 API_WORKSPACE"
|
||||
- "如何新增 API endpoint 文檔"
|
||||
- "make deploy 流程"
|
||||
- "自定義交付文件"
|
||||
related_documents:
|
||||
- "STANDARDS/USER_DOCS_STANDARD.md"
|
||||
- "STANDARDS/DOCS_STANDARD.md"
|
||||
- "API_WORKSPACE/README.md"
|
||||
- "API_WORKSPACE/modules/_template.md"
|
||||
---
|
||||
|
||||
# 模組生成式文件產出系統
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-05-17 |
|
||||
| 文件版本 | V1.0 |
|
||||
| 目標讀者 | developer, documentation maintainer |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-05-17 | 建立設計文件 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計理念
|
||||
|
||||
### 1.1 痛點
|
||||
|
||||
傳統 API 文件維護有常見問題:
|
||||
|
||||
| 問題 | 具體表現 |
|
||||
|------|----------|
|
||||
| **內容重複** | 同一個 endpoint 在快速參考、完整手冊、教育訓練文件中寫三次 |
|
||||
| **更新遺漏** | 修改 curl 範例後,忘記同步到另一份文件 |
|
||||
| **交付僵化** | 無法按對象產出不同版本的 API 文件 |
|
||||
| **版本失靈** | YAML frontmatter 版本號與實際內容脫節 |
|
||||
|
||||
### 1.2 核心原則
|
||||
|
||||
```
|
||||
單一真理源(modules/)→ 組裝引擎(assemble_docs.sh)→ 多種交付產品(GUIDES/)
|
||||
|
||||
編輯 ──→ 生成 ──→ 部署
|
||||
1 處修改模組 make all make deploy
|
||||
```
|
||||
|
||||
| 原則 | 說明 |
|
||||
|------|------|
|
||||
| **單一真理源** | 每個 endpoint 只在 `modules/` 中定義一次 |
|
||||
| **組裝而非撰寫** | 交付文件是 modules 的組合,不是手寫 |
|
||||
| **開發與交付分離** | `API_WORKSPACE/` 開發,`GUIDES/` 交付 |
|
||||
| **模組為最小可測試單位** | 每個 module 可獨立驗證正確性 |
|
||||
| **配置驅動** | `.toml` 配置定義哪些 module 以何種模式組裝成何種輸出 |
|
||||
|
||||
### 1.3 檔案類型對照
|
||||
|
||||
| 類型 | 角色 | 可編輯 | 位置 |
|
||||
|------|------|--------|------|
|
||||
| Module (模組) | 不可再拆的內容最小單位 | ✅ 是 | `API_WORKSPACE/modules/` |
|
||||
| Config (配方) | 定義組裝規則 | ✅ 是 | `API_WORKSPACE/configs/` |
|
||||
| Narrative (敘事) | 非結構化的前言/背景 | ✅ 是 | `API_WORKSPACE/narratives/` |
|
||||
| Assembled (產出) | 從模組組裝的交付文件 | ❌ 否(generated) | `API_WORKSPACE/_build/` → `GUIDES/` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目錄結構
|
||||
|
||||
```
|
||||
docs_v1.0/
|
||||
├── API_WORKSPACE/ ← 開發區
|
||||
│ ├── modules/ ← 端點模組(單一真理源)
|
||||
│ │ ├── _template.md ← 模組撰寫規範
|
||||
│ │ ├── 01_auth.md ← 認證、Base URL
|
||||
│ │ ├── 02_health.md ← 健康檢查
|
||||
│ │ ├── 03_register.md ← 註冊、掃描
|
||||
│ │ ├── 04_lookup.md ← 查詢、刪除
|
||||
│ │ ├── 05_process.md ← 處理、進度、任務
|
||||
│ │ ├── 06_search.md ← 搜尋(向量、n8n、視覺)
|
||||
│ │ ├── 07_identity.md ← 身份 CRUD、bind/unbind
|
||||
│ │ ├── 08_identity_agent.md ← Identity Agent
|
||||
│ │ ├── 09_tmdb.md ← TMDb Enrichment
|
||||
│ │ ├── 10_pipeline.md ← Stats、配置、未掛載端點
|
||||
│ │ └── 11_error_codes.md ← 錯誤碼對照表
|
||||
│ │
|
||||
│ ├── configs/ ← 組裝配方(每個輸出一份)
|
||||
│ │ ├── reference.toml → API_REFERENCE.md
|
||||
│ │ ├── endpoints.toml → API_ENDPOINTS.md
|
||||
│ │ ├── quickref.toml → API_QUICK_REFERENCE.md
|
||||
│ │ ├── errors.toml → API_ERROR_CODES.md
|
||||
│ │ ├── index.toml → API_INDEX.md
|
||||
│ │ ├── marcom.toml → API_TRAINING_MARCOM.md
|
||||
│ │ └── tmdb.toml → TMDb_User_Guide.md
|
||||
│ │
|
||||
│ ├── narratives/ ← 非端點敘事前言
|
||||
│ │ └── marcom_intro.md
|
||||
│ │
|
||||
│ ├── _build/ ← 生成暫存區(gitignored)
|
||||
│ ├── Makefile ← 組裝自動化入口
|
||||
│ ├── assemble_docs.sh ← 組裝引擎
|
||||
│ └── README.md ← 開發者速查
|
||||
│
|
||||
├── GUIDES/ ← 交付區
|
||||
│ ├── API_REFERENCE.md (generated)
|
||||
│ ├── API_ENDPOINTS.md (generated)
|
||||
│ ├── API_QUICK_REFERENCE.md (generated)
|
||||
│ ├── API_ERROR_CODES.md (generated)
|
||||
│ ├── API_INDEX.md (generated)
|
||||
│ ├── API_TRAINING_MARCOM.md (generated)
|
||||
│ ├── TMDb_User_Guide.md (generated)
|
||||
│ ├── Demo_EndToEnd.md (手寫保留)
|
||||
│ ├── Pipeline_API_Demo.md (手寫保留)
|
||||
│ └── ... (其他手寫文件)
|
||||
│
|
||||
├── DESIGN/
|
||||
├── REFERENCE/
|
||||
├── OPERATIONS/
|
||||
├── INTEGRATIONS/
|
||||
└── STANDARDS/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模組規範
|
||||
|
||||
### 3.1 檔名規則
|
||||
|
||||
- 格式:`NN_<name>.md`(NN = 兩位數排序 01-99)
|
||||
- 範例:`03_register.md`, `09_tmdb.md`
|
||||
- 依賴序號決定組裝時的 endpoint 順序
|
||||
|
||||
### 3.2 Module Metadata 註解
|
||||
|
||||
每個 module 開頭必須有 metadata 註解:
|
||||
|
||||
```markdown
|
||||
<!-- module: auth -->
|
||||
<!-- description: Authentication, API Key, Base URL configuration -->
|
||||
<!-- depends: -->
|
||||
```
|
||||
|
||||
| 欄位 | 必填 | 說明 |
|
||||
|------|------|------|
|
||||
| `module` | Yes | 唯一名稱,無空格無數字開頭 |
|
||||
| `description` | Yes | 一句話說明 |
|
||||
| `depends` | No | 依賴的其他 module 名稱(逗號分隔) |
|
||||
|
||||
### 3.3 Endpoint 結構
|
||||
|
||||
每個 endpoint 必須使用一致結構:
|
||||
|
||||
```markdown
|
||||
### `METHOD /path/to/endpoint`
|
||||
|
||||
**Auth**: Required / Optional / Public
|
||||
**Scope**: file-level / identity-level / system-level
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X METHOD "$API/path" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"field": "value"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{ ... }
|
||||
```
|
||||
|
||||
#### Error Codes
|
||||
|
||||
| Code | HTTP | When |
|
||||
|------|------|------|
|
||||
```
|
||||
```
|
||||
|
||||
### 3.4 變數規則
|
||||
|
||||
| 變數 | 用途 | 範例值 |
|
||||
|------|------|--------|
|
||||
| `$API` | Base URL | `http://localhost:3003` |
|
||||
| `$KEY` | API Key | `your-api-key-here` |
|
||||
| `$FILE_UUID` | File UUID | `3a6c1865...` |
|
||||
| `$IDENTITY_UUID` | Identity UUID | `a9a90105...` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 組裝引擎
|
||||
|
||||
### 4.1 `assemble_docs.sh`
|
||||
|
||||
Shell 腳本,接收三個參數:
|
||||
|
||||
| 參數 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `--config` | TOML 配方路徑 | `configs/reference.toml` |
|
||||
| `--modules` | Module 目錄 | `modules/` |
|
||||
| `--build` | 輸出目錄 | `_build/` |
|
||||
|
||||
### 4.2 三種組裝模式
|
||||
|
||||
| mode | 行為 | 適用 |
|
||||
|------|------|------|
|
||||
| `full` | 完整包含 module 全部內容(除 metadata) | API_REFERENCE, API_ENDPOINTS |
|
||||
| `summary` | 僅擷取 endpoint 表格 + curl 範例 | API_QUICK_REFERENCE |
|
||||
| `index` | 生成文件總覽(掃描 modules 目錄自動產生索引) | API_INDEX |
|
||||
|
||||
### 4.3 組裝流程
|
||||
|
||||
```
|
||||
1. 讀取 config.toml → 解析 title, modules, mode, narrative
|
||||
2. 生成 YAML frontmatter(含 document_type, date, version)
|
||||
3. 生成 title heading + info block
|
||||
4. (可選)摘自 TOC:從 modules ## headings 生成目錄
|
||||
5. (可選)插入 narrative intro
|
||||
6. 遍歷 modules:
|
||||
- full mode: 複製整份內容(跳過 <!-- --> 註解)
|
||||
- summary mode: 只提取 | table | + ```bash code block
|
||||
- index mode: 自動掃描 modules 目錄生成清單
|
||||
7. 寫入 _build/ 輸出檔案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配方格式(config.toml)
|
||||
|
||||
```toml
|
||||
title = "輸出文件標題"
|
||||
output = "_build/FILENAME.md" # 輸出路徑(相對於 API_WORKSPACE)
|
||||
mode = "full" # full | summary | index
|
||||
modules = ["01_auth", "03_register"] # 要包含的 module 名稱
|
||||
narrative = "narratives/xxx.md" # (可選)包含的敘事前言
|
||||
toc = true # (可選)是否生成目錄
|
||||
|
||||
[frontmatter]
|
||||
document_type = "api_reference" # 用於 YAML frontmatter
|
||||
service = "MOMENTRY_CORE"
|
||||
version = "V1.0"
|
||||
owner = "M5"
|
||||
created_by = "OpenCode"
|
||||
```
|
||||
|
||||
### 內建配方一覽
|
||||
|
||||
| 檔案 | 輸出 | Modules | Mode |
|
||||
|------|------|---------|------|
|
||||
| `reference.toml` | API_REFERENCE.md | 01-11 | full |
|
||||
| `endpoints.toml` | API_ENDPOINTS.md | 01-10 | full |
|
||||
| `quickref.toml` | API_QUICK_REFERENCE.md | 01-06,09 | summary |
|
||||
| `errors.toml` | API_ERROR_CODES.md | 11 | full |
|
||||
| `index.toml` | API_INDEX.md | (auto) | index |
|
||||
| `marcom.toml` | API_TRAINING_MARCOM.md | 01,03,06 + narrative | full |
|
||||
| `tmdb.toml` | TMDb_User_Guide.md | 01,03,09 | full |
|
||||
|
||||
---
|
||||
|
||||
## 6. 工作流程
|
||||
|
||||
### 6.1 日常修改
|
||||
|
||||
```bash
|
||||
# 1. 編輯模組
|
||||
cd API_WORKSPACE
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 2. 重新生成單一文件
|
||||
make tmdb
|
||||
|
||||
# 3. 預覽結果
|
||||
less _build/TMDb_User_Guide.md
|
||||
|
||||
# 4. 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
### 6.2 新增端點
|
||||
|
||||
```bash
|
||||
# 1. 找到所屬模組
|
||||
ls modules/
|
||||
# 決定該 endpoint 屬於哪個模組(如 tmdb, identity, search)
|
||||
|
||||
# 2. 在對應模組加入 endpoint 文檔
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 3. 重新生成所有文件
|
||||
make all
|
||||
|
||||
# 4. 確認所有引用此端點的文件都有正確更新
|
||||
make check
|
||||
|
||||
# 5. 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
### 6.3 客製化交付
|
||||
|
||||
```bash
|
||||
# 新增一個客製化配方
|
||||
cat > configs/integration_partner.toml << TOML
|
||||
title = "Integration Partner API Guide"
|
||||
output = "_build/PARTNER_GUIDE.md"
|
||||
mode = "full"
|
||||
modules = ["01_auth", "06_search", "09_tmdb", "11_error_codes"]
|
||||
toc = true
|
||||
[frontmatter]
|
||||
document_type = "user_manual"
|
||||
service = "MOMENTRY_CORE"
|
||||
version = "V1.0"
|
||||
owner = "M5"
|
||||
created_by = "OpenCode"
|
||||
TOML
|
||||
|
||||
# 在 Makefile 中加入對應 target
|
||||
echo "partner:" >> Makefile
|
||||
echo ' @$$(SCRIPT) --config configs/integration_partner.toml --modules $$(MODULES) --build $$(BUILD)' >> Makefile
|
||||
|
||||
# 生成
|
||||
make partner
|
||||
|
||||
# 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付客製化對照表
|
||||
|
||||
| 對象 | 需要 modules | make target | 輸出 |
|
||||
|------|-------------|-------------|------|
|
||||
| API Developer | 01-11 (all) | `make reference` | API_REFERENCE.md |
|
||||
| Quick Start User | 01-06,09 | `make quickref` | API_QUICK_REFERENCE.md |
|
||||
| Marcom Team | 01,03,06 + narrative | `make marcom` | API_TRAINING_MARCOM.md |
|
||||
| TMDb User | 01,03,09 | `make tmdb` | TMDb_User_Guide.md |
|
||||
| Integration Partner | 01,06,09,11 | Custom config | PARTNER_GUIDE.md |
|
||||
|
||||
---
|
||||
|
||||
## 8. GUIDES/ 文件類型說明
|
||||
|
||||
| 類型 | 來源 | 說明 |
|
||||
|------|------|------|
|
||||
| `API_*.md` (7 files) | Generated from API_WORKSPACE | API 功能文件,endpoint 列表 + curl 範例 |
|
||||
| `Demo_*.md`, `M5API_*.md` | 手寫 | 敘事性指引,含完整 step-by-step 流程 |
|
||||
| `PORTAL_*.md` | 手寫 | Portal 開發計畫與 Demo 指引 |
|
||||
| `USER_MANUAL.md` | 手寫 | 系統操作使用手冊 |
|
||||
|
||||
> **提醒**:不要直接修改 GUIDES/ 中的 generated files。修改應在 API_WORKSPACE/modules/ 中進行,然後執行 `make deploy`。
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- `API_WORKSPACE/README.md` — 開發者快速上手指南
|
||||
- `API_WORKSPACE/modules/_template.md` — 模組撰寫範本
|
||||
- `STANDARDS/DOCS_STANDARD.md` — 文件創建規範
|
||||
- `STANDARDS/USER_DOCS_STANDARD.md` — 使用者文件規範
|
||||
128
docs_v1.0/DESIGN/REPRESENTATIVE_FRAME_API_V1.md
Normal file
128
docs_v1.0/DESIGN/REPRESENTATIVE_FRAME_API_V1.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Representative Frame API V1.0
|
||||
|
||||
Portal 影片代表畫面 API — 沒有指定 frame_number 時自動偵測男女主角找到最佳互動 frame。
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### Purpose
|
||||
|
||||
Portal 需要為每個影片顯示一張代表畫面(thumbnail),內容應為該影片最具代表性的 scene — 通常包含男女主角同框且互看的時刻。
|
||||
|
||||
### Principle
|
||||
|
||||
**沒有指定 frame_number → auto-detect representative frame**
|
||||
|
||||
既有端點不需改動,只需在 `frame` 參數為空時自動偵測。
|
||||
|
||||
---
|
||||
|
||||
## 2. Endpoint
|
||||
|
||||
### `GET /api/v1/file/:file_uuid/thumbnail`
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `frame` | i64 | ❌ | 指定 frame;不傳則 auto-detect |
|
||||
| `x` | i32 | ❌ | bbox crop x |
|
||||
| `y` | i32 | ❌ | bbox crop y |
|
||||
| `w` | i32 | ❌ | bbox crop width |
|
||||
| `h` | i32 | ❌ | bbox crop height |
|
||||
|
||||
**Response**: Pure JPEG bytes (Content-Type: image/jpeg)
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
GET /api/v1/file/:uuid/thumbnail → auto-detect
|
||||
GET /api/v1/file/:uuid/thumbnail?frame=38165 → 指定 frame
|
||||
GET /api/v1/file/:uuid/thumbnail?frame=38165&x=723&y=205&w=221&h=221 → 指定 crop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Internal Algorithm
|
||||
|
||||
### Auto-detect Fallback Chain
|
||||
|
||||
```
|
||||
Step 1: Auto-detect 主角 (top 2 by face count)
|
||||
└─ face_detections JOIN identities
|
||||
|
||||
Step 2: TKG Bridge — mutual_gaze?
|
||||
├── 有 mutual_gaze edge → first_frame ✅
|
||||
└── 無 → face_detections 第一次同框 frame ✅
|
||||
|
||||
Step 3: 只有一個主角?
|
||||
└─ 該主角 face_quality (w×h×confidence) 最高 frame
|
||||
|
||||
Step 4: 完全無 identity?
|
||||
└─ 任 identity 的 face_quality 最高 frame
|
||||
|
||||
Step 5: 完全無 face?
|
||||
└─ 404 "No faces in this file"
|
||||
```
|
||||
|
||||
### TKG Bridge Query
|
||||
|
||||
```sql
|
||||
-- 找兩主角各自的 main trace
|
||||
SELECT trace_id FROM face_detections
|
||||
WHERE file_uuid = $1 AND identity_id = $2 AND trace_id IS NOT NULL
|
||||
GROUP BY trace_id ORDER BY COUNT(*) DESC LIMIT 1;
|
||||
|
||||
-- TKG mutual_gaze 查詢
|
||||
SELECT (e.properties->>'first_frame')::bigint
|
||||
FROM tkg_edges e
|
||||
JOIN tkg_nodes a ON a.id = e.source_node_id
|
||||
JOIN tkg_nodes b ON b.id = e.target_node_id
|
||||
WHERE e.file_uuid = $1
|
||||
AND a.external_id = concat('trace_', $4)
|
||||
AND b.external_id = concat('trace_', $5)
|
||||
AND e.properties->>'mutual_gaze' = 'true'
|
||||
LIMIT 1;
|
||||
|
||||
-- Fallback: 第一次同框
|
||||
SELECT MIN(fd_a.frame_number)::bigint
|
||||
FROM face_detections fd_a
|
||||
JOIN face_detections fd_b ON fd_a.frame_number = fd_b.frame_number
|
||||
WHERE fd_a.file_uuid = $1 AND fd_a.identity_id = $2 AND fd_b.identity_id = $3;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation
|
||||
|
||||
### Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/api/media_api.rs` | `ThumbQuery.frame` → `Option<i64>`; add auto-detect fallback |
|
||||
| `src/core/processor/tkg.rs` | Add `query_auto_representative_frame()` + structs (已實作) |
|
||||
| `src/core/processor/mod.rs` | Export new function + structs (已實作) |
|
||||
|
||||
### Existing Trace-level Endpoints (不變)
|
||||
|
||||
```
|
||||
GET /api/v1/file/:uuid/trace/:tid/representative-face → JSON (legacy)
|
||||
GET /api/v1/file/:uuid/trace/:tid/thumbnail → JPEG (auto via select_rep_face)
|
||||
```
|
||||
|
||||
### No Changes
|
||||
|
||||
- ❌ No new DB tables / migrations
|
||||
- ❌ No changes to `select_rep_face` / blurdetect
|
||||
- ❌ No chunk / cut / pre_chunks dependency
|
||||
|
||||
---
|
||||
|
||||
## 5. Version History
|
||||
|
||||
| Date | Version | Author | Change |
|
||||
|------|---------|--------|--------|
|
||||
| 2026-05-22 | 1.0 | OpenCode | Initial design |
|
||||
| 2026-05-22 | 1.1 | OpenCode | 簡化為單一 endpoint: frame 為 None 時 auto-detect |
|
||||
|
||||
*Updated: 2026-05-22*
|
||||
270
docs_v1.0/DESIGN/Redis_Progress_Reporting_V1.0.md
Normal file
270
docs_v1.0/DESIGN/Redis_Progress_Reporting_V1.0.md
Normal file
@@ -0,0 +1,270 @@
|
||||
---
|
||||
document_type: "design_doc"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Redis Progress Reporting V1.0"
|
||||
version: "V1.0"
|
||||
date: "2026-05-17"
|
||||
author: "M5"
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
# Redis Progress Reporting V1.0
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| Service | `MOMENTRY_CORE` |
|
||||
| Version | V1.0 |
|
||||
| Date | 2026-05-17 |
|
||||
| Author | M5 (OpenCode) |
|
||||
| Status | Draft |
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This document defines the standardized progress reporting architecture for Momentry Core processors. It replaces the inconsistent ad-hoc progress patterns found across `scripts/`, `src/worker/`, and `src/api/`.
|
||||
|
||||
### 1.1 Problems Addressed
|
||||
|
||||
| # | Problem | Detail |
|
||||
|---|---------|--------|
|
||||
| 1 | Worker Redis key does not match `OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md` V1.0 spec | Worker writes `worker:job:{uuid}:processor:{name}` instead of spec `job:{uuid}:processor:{name}` |
|
||||
| 2 | Progress API reads wrong key | `get_progress()` reads `worker:job:{uuid}:processor:{name}` — unresolved with Playground subscriber which writes `job:{uuid}:processor:{name}` |
|
||||
| 3 | Swift processors (Face/OCR/Pose) lack RedisPublisher | Progress lost — only stdout text |
|
||||
| 4 | ASRX/Story/Visual chunk have no incremental progress | Start + complete only, no `current/total` updates |
|
||||
| 5 | `frames_processed` / `chunks_produced` never updated in real-time | Worker only writes processor hash at start and exit |
|
||||
| 6 | No `output_count` / `output_type` fields | Impossible to know how many faces/objects/segments were produced |
|
||||
|
||||
### 1.2 Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Progress unit = frames for video processors | All media-level processors work frame by frame |
|
||||
| Output count separate from progress | Processors may produce N outputs per frame (multiple faces, objects) |
|
||||
| Pub/sub for real-time, Hash for final state | Pub/sub is transient; Hash persists for API queries |
|
||||
|
||||
---
|
||||
|
||||
## 2. Redis Key Architecture
|
||||
|
||||
### 2.1 Key Patterns
|
||||
|
||||
All keys use the configured `REDIS_KEY_PREFIX` (default: `momentry:` for production, `momentry_dev:` for playground).
|
||||
|
||||
| Pattern | Type | TTL | Purpose | Owner |
|
||||
|---------|------|-----|---------|-------|
|
||||
| `{prefix}progress:{uuid}` | Pub/Sub | — | Real-time progress messages | Python scripts |
|
||||
| `{prefix}job:{uuid}` | Hash | 24h | Per-video job state | Worker |
|
||||
| `{prefix}job:{uuid}:processor:{name}` | Hash | 24h | Per-processor final state | Worker |
|
||||
| `{prefix}job:{uuid}:processor:{name}:output_count` | String | 24h | Output count by type | Worker |
|
||||
|
||||
### 2.2 Processor Hash Fields
|
||||
|
||||
```
|
||||
{prefix}job:{uuid}:processor:{name}
|
||||
├── status String running / completed / failed / pending
|
||||
├── current u32 Units processed (frames for video processors)
|
||||
├── total u32 Total units
|
||||
├── output_count u32 Output items produced (faces, objects, segments)
|
||||
├── output_type String Type name of output: faces / objects / segments / cuts / etc.
|
||||
├── pid i32 OS process ID (0 if not running)
|
||||
├── error String Error message if failed
|
||||
└── updated_at String ISO 8601 timestamp
|
||||
```
|
||||
|
||||
### 2.3 Migrated Keys
|
||||
|
||||
The following key patterns from the original implementation are REMOVED:
|
||||
|
||||
| Old Key | Reason |
|
||||
|---------|--------|
|
||||
| `{prefix}worker:job:{uuid}:processor:{name}` | Non-standard prefix — not in `MOMENTRY_CORE_REDIS_KEYS.md` spec |
|
||||
| `{prefix}job:{uuid}:processor:{name}:status` (flat) | Redundant — status stored in Hash |
|
||||
| `{prefix}job:{uuid}:processor:{name}:progress` (flat) | Replaced by `current` + `total` for percent calculation |
|
||||
| `{prefix}job:{uuid}:processor:{name}:current` (flat) | Replaced by Hash fields |
|
||||
| `{prefix}job:{uuid}:processor:{name}:total` (flat) | Replaced by Hash fields |
|
||||
| `{prefix}job:{uuid}:processor:{name}:started_at` (flat) | Replaced by Hash `updated_at` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Pub/Sub Message Format
|
||||
|
||||
### 3.1 Channel
|
||||
|
||||
```
|
||||
{prefix}progress:{uuid}
|
||||
```
|
||||
|
||||
### 3.2 Message JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"processor": "face",
|
||||
"current": 150,
|
||||
"total": 162696,
|
||||
"output_count": 423,
|
||||
"output_type": "faces",
|
||||
"message": "Processing frame 150",
|
||||
"timestamp": 1700000000
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Field Definitions
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `processor` | String | ✅ | Processor name: asr / asrx / yolo / ocr / face / pose / cut / story / visual_chunk |
|
||||
| `current` | u32 | ✅ | Units processed (frames for video processors) |
|
||||
| `total` | u32 | ✅ | Total units |
|
||||
| `output_count` | u32 | ❌ | Output items produced so far |
|
||||
| `output_type` | String | ❌ | Type name: faces / objects / segments / cuts / text_regions / persons / speakers / stories / visual_chunks |
|
||||
| `message` | String | ❌ | Human-readable progress description |
|
||||
| `timestamp` | u64 | ✅ | Unix timestamp |
|
||||
|
||||
---
|
||||
|
||||
## 4. Per-Processor Metrics
|
||||
|
||||
| Processor | current/total Unit | output_type | When to Publish |
|
||||
|-----------|-------------------|-------------|-----------------|
|
||||
| ASR | frames | `segments` | Every 100 segments processed |
|
||||
| ASRX | frames | `speakers` | Every processing stage |
|
||||
| YOLO | frames | `objects` | Every 500 frames |
|
||||
| OCR | frames | `text_regions` | Every 5% |
|
||||
| Face | frames | `faces` | Every batch (5% of frames) |
|
||||
| Pose | frames | `persons` | Every 10% |
|
||||
| CUT | frames | `cuts` | Every scene detected |
|
||||
| Story | chunks | `stories` | Every chunk processed |
|
||||
| Visual chunk | frames | `visual_chunks` | Every chunk processed |
|
||||
|
||||
### 4.1 Output Type Enum
|
||||
|
||||
```rust
|
||||
pub enum OutputType {
|
||||
Segments, // ASR
|
||||
Speakers, // ASRX
|
||||
Objects, // YOLO
|
||||
TextRegions, // OCR
|
||||
Faces, // Face
|
||||
Persons, // Pose
|
||||
Cuts, // CUT
|
||||
Stories, // Story
|
||||
VisualChunks, // Visual chunk
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
```
|
||||
┌──────────────────┐ Pub/Sub ┌──────────────────────┐
|
||||
│ Python Processor │ ───────── progress:{uuid} ──────────→│ Worker (subscriber) │
|
||||
│ (ASR/YOLO/Face) │ {current, total, │ │
|
||||
│ │ output_count, output_type} │ ──→ HSET │
|
||||
└──────────────────┘ │ job:{uuid}: │
|
||||
│ processor:{name} │
|
||||
┌──────────────────┐ │ │
|
||||
│ Swift Processor │ ──→ Python wrapper ──→ pub/sub │ (status, current, │
|
||||
│ (Face/OCR/Pose) │ (add RedisPublisher) │ total, output_count,│
|
||||
└──────────────────┘ │ output_type) │
|
||||
└──────────┬───────────┘
|
||||
│ HGETALL
|
||||
┌──────────▼───────────┐
|
||||
│ Progress API │
|
||||
│ GET /progress/:uuid │
|
||||
│ │
|
||||
│ ─→ compute % │
|
||||
│ ─→ return JSON │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Plan
|
||||
|
||||
### Phase 1: Python Processor RedisPublisher
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Add `RedisPublisher` to `face_processor.py` | `scripts/face_processor.py` | Medium |
|
||||
| Add `RedisPublisher` to `ocr_processor.py` | `scripts/ocr_processor.py` | Medium |
|
||||
| Add `RedisPublisher` to `pose_processor.py` | `scripts/pose_processor.py` | Medium |
|
||||
| Add incremental `.progress()` to `asrx_processor_custom.py` | `scripts/asrx_processor_custom.py` | Low |
|
||||
| Standardize pub/sub message to include `output_count`, `output_type` | All processor scripts | Low |
|
||||
|
||||
### Phase 2: Worker
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Fix Redis key from `worker:job:` to `job:` | `src/worker/processor.rs`, `src/core/db/redis_client.rs` | Low |
|
||||
| Subscribe to `progress:{uuid}` channel in `run_processor()` | `src/worker/processor.rs` | Medium |
|
||||
| HSET Processor Hash on each progress message | `src/worker/processor.rs` | Medium |
|
||||
| Set `output_count` and `output_type` from pub/sub message | `src/worker/processor.rs` | Low |
|
||||
|
||||
### Phase 3: Progress API
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Read `output_count`, `output_type` from Redis Hash | `src/api/server.rs` | Low |
|
||||
| Compute percentage from `current` / `total` | `src/api/server.rs` | Low |
|
||||
| Return `output_count`, `output_type` in response JSON | `src/api/server.rs` | Low |
|
||||
| Remove `worker:` fallback path | `src/api/server.rs` | Low |
|
||||
|
||||
### Phase 4: Cleanup
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Remove old `worker:job:` keys from Redis | Deployment script | Low |
|
||||
| Remove `update_processor_progress()` DB path (stale `processing_status` JSONB) | `src/core/db/postgres_db.rs` | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 7. API Response Changes
|
||||
|
||||
### ProgressResponse (new fields)
|
||||
|
||||
```json
|
||||
{
|
||||
"processors": [
|
||||
{
|
||||
"name": "face",
|
||||
"status": "running",
|
||||
"current": 150,
|
||||
"total": 162696,
|
||||
"progress": 0,
|
||||
"frames_processed": 150,
|
||||
"output_count": 423,
|
||||
"output_type": "faces"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Dependencies
|
||||
|
||||
| Component | Version | Role |
|
||||
|-----------|---------|------|
|
||||
| Redis | ≥ 6.0 | Pub/Sub + Hash storage |
|
||||
| `redis_publisher.py` | Existing | Python → Redis pub/sub client |
|
||||
| `redis_client.rs` | Existing | Rust Redis client for worker + API |
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
| Doc | Relation |
|
||||
|-----|----------|
|
||||
| `OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md` | Parent spec — this doc supersedes sections 4, 7, 8 |
|
||||
| `DESIGN/VIDEO_PROCESSING_SPEC.md` §2.3 | Original progress design (ProcessProgress struct) |
|
||||
| `src/worker/processor.rs` | Worker progress write implementation |
|
||||
| `scripts/redis_publisher.py` | Python pub/sub client |
|
||||
| `src/api/server.rs` (get_progress) | Progress API handler |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Change |
|
||||
|---------|------|--------|--------|
|
||||
| V1.0 | 2026-05-17 | M5 (OpenCode) | Initial draft — replaces ad-hoc progress patterns |
|
||||
242
docs_v1.0/M4_workspace/2026-05-27_charade_pipeline_checklist.md
Normal file
242
docs_v1.0/M4_workspace/2026-05-27_charade_pipeline_checklist.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Charade Full Movie Pipeline Checklist
|
||||
version: 1.0
|
||||
date: 2026-05-27
|
||||
author: M5Max48
|
||||
status: in_progress
|
||||
---
|
||||
|
||||
# Charade Full Movie Pipeline Checklist
|
||||
|
||||
**File UUID**: `c3c635e3641da80dde10cc555ffcdda5`
|
||||
**File Name**: Charade (1963) Cary Grant & Audrey Hepburn | Comedy Mystery Romance Thriller | Full Movie.mp4
|
||||
**Duration**: 6785 seconds (113 minutes)
|
||||
**Total Frames**: 169,625
|
||||
|
||||
---
|
||||
|
||||
## P0: Processor Outputs
|
||||
|
||||
### Purpose
|
||||
原始處理器輸出檔案,存放在 `/Users/accusys/momentry/output_dev/`。這些是後續 ingestion 的資料來源。
|
||||
|
||||
### Processor Details
|
||||
|
||||
| Processor | Expected Output | Size Estimate | Purpose | Status |
|
||||
|-----------|-----------------|---------------|---------|--------|
|
||||
| CUT | `c3c635e3641da80dde10cc555ffcdda5.cut.json` | ~170KB | Scene boundary detection,切割點用於 Rule 3 chunking | ✅ Done |
|
||||
| YOLO | `c3c635e3641da80dde10cc555ffcdda5.yolo.json` | ~50-80MB | Object detection,每幀的物件類別與位置 | 🔄 Running |
|
||||
| Face | `c3c635e3641da80dde10cc555ffcdda5.face.json` | ~1.5GB | Face detection + 512-dim embedding (FaceNet CoreML) | 🔄 44% |
|
||||
| Face Traced | `c3c635e3641da80dde10cc555ffcdda5.face_traced.json` | ~1.2GB | Face tracking,同一人物的連續出現 → trace_id | ⏳ Pending (after Face) |
|
||||
| OCR | `c3c635e3641da80dde10cc555ffcdda5.ocr.json` | ~50KB | Text recognition from frames | ❌ Skipped |
|
||||
| Pose | `c3c635e3641da80dde10cc555ffcdda5.pose.json` | ~20MB | Body pose estimation | 🔄 Running |
|
||||
| ASRX | `c3c635e3641da80dde10cc555ffcdda5.asrx.json` | ~8MB | Speaker diarization,語者分段 | ✅ Done (reuse from public) |
|
||||
| Visual Chunk | `c3c635e3641da80dde10cc555ffcdda5.visual_chunk.json` | ~60KB | Visual scene chunk metadata | ✅ Done |
|
||||
| Scene | `c3c635e3641da80dde10cc555ffcdda5.scene.json` | ~300B | Scene list from CUT | ✅ Done |
|
||||
| Scene Meta | `c3c635e3641da80dde10cc555ffcdda5.scene_meta.json` | ~50KB | Heuristic scene metadata (人物 + 物件統計) | ⏳ Pending |
|
||||
| Story LLM | `c3c635e3641da80dde10cc555ffcdda5.story_llm.json` | ~800KB | LLM-generated story summaries per chunk | ✅ Done |
|
||||
| Story Story | `c3c635e3641da80dde10cc555ffcdda5.story_story.json` | ~800KB | Story parent-child relationships | ✅ Done |
|
||||
| TMDb | `c3c635e3641da80dde10cc555ffcdda5.tmdb.json` | ~5KB | TMDb cast list with face embeddings | ⏳ Pending |
|
||||
| 5W1H | `c3c635e3641da80dde10cc555ffcdda5.5w1h.json` | ~500KB | 5W1H agent output (who/when/where/what/why/how) | ✅ Done |
|
||||
|
||||
### Key Dependencies
|
||||
- Face Traced 需要 Face 完成後才能執行 (face_traced.json = face.json + tracking)
|
||||
- Scene Meta 需要 Face + YOLO 完成
|
||||
- TMDb 需要 Face Traced 完成後執行 matching
|
||||
|
||||
---
|
||||
|
||||
## P1: Database Records
|
||||
|
||||
### Purpose
|
||||
將 processor outputs 存入 PostgreSQL,供 API query 使用。
|
||||
|
||||
### Table Details
|
||||
|
||||
| Table | Expected Records | Purpose | Verification Query | Status |
|
||||
|-------|------------------|---------|-------------------|--------|
|
||||
| `dev.videos` | 1 row | Video metadata (duration, fps, status) | `SELECT file_uuid, status FROM dev.videos WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ✅ Registered |
|
||||
| `dev.monitor_jobs` | 1 row | Processing job state machine | `SELECT uuid, status, completed_processors FROM dev.monitor_jobs WHERE uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | 🔄 Running |
|
||||
| `dev.pre_chunks` | ~7,000 rows | Raw processor outputs (ASR sentences, YOLO objects, etc.) | `SELECT COUNT(*) FROM dev.pre_chunks WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections` | ~70,000 rows | Face detection records (每幀每張臉) | `SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.embedding` | ~70,000 non-NULL | 512-dim FaceNet embedding (用於 identity matching) | `SELECT COUNT(embedding) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.trace_id` | ~70,000 non-NULL | Face tracking ID (同一人物跨幀連續出現) | `SELECT COUNT(trace_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.identity_id` | ~50,000 non-NULL | TMDb identity binding (Audrey, Cary, etc.) | `SELECT COUNT(identity_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
|
||||
### Key Points
|
||||
- `embedding` 必須非 NULL 才能進行 TMDb matching (之前 store_traced_faces.py bug 修復)
|
||||
- `trace_id` 由 `store_traced_faces.py` 從 face_traced.json 計算
|
||||
- `identity_id` 由 `match_faces_to_tmdb.py` 計算 (cosine similarity > 0.5)
|
||||
|
||||
---
|
||||
|
||||
## P2: Chunk Ingestion
|
||||
|
||||
### Purpose
|
||||
將 raw processor outputs 轉換為 searchable chunks,用於 RAG query。
|
||||
|
||||
### Chunk Types
|
||||
|
||||
| Chunk Type | Expected Count | Purpose | Source | Verification Query | Status |
|
||||
|------------|----------------|---------|--------|-------------------|--------|
|
||||
| sentence (Rule 1) | ~1,700 | Sentence-level chunks for text search | ASR output → sentence split | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'sentence'` | ⏳ Pending |
|
||||
| llm_parent | ~800 | LLM-generated summary parent chunks | Story LLM output | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'llm_parent'` | ⏳ Pending |
|
||||
| story_parent | ~800 | Story parent chunks (narrative segments) | Story processor | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'story_parent'` | ⏳ Pending |
|
||||
| story_child | ~1,700 | Story child chunks (linked to sentence) | Story processor | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'story_child'` | ⏳ Pending |
|
||||
| cut (Rule 3) | ~500 | Scene-level chunks for scene search | CUT output → scene boundaries | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'cut'` | ⏳ Pending |
|
||||
| trace | ~3,600 | Face trace chunks (identity-centric) | Face Traced output | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'trace'` | ⏳ Pending |
|
||||
|
||||
### Ingestion Pipeline
|
||||
1. **Rule 1**: ASR → sentence split → chunk + embedding → Qdrant
|
||||
2. **Rule 3**: CUT + ASR → scene chunks → chunk + embedding → Qdrant
|
||||
3. **Trace**: Face Traced → trace chunks → TKG nodes → Qdrant
|
||||
|
||||
### Key Points
|
||||
- `start_frame` / `end_frame` 必須正確計算 (之前 bug: frame=0)
|
||||
- Chunks 必須有 `embedding` 才能 search
|
||||
|
||||
---
|
||||
|
||||
## P3: Vector Embeddings
|
||||
|
||||
### Purpose
|
||||
將 chunks 的 text 轉換為 768-dim embeddings,存入 PostgreSQL + Qdrant,用於 semantic search。
|
||||
|
||||
### Embedding Targets
|
||||
|
||||
| Target | Expected Count | Model | Purpose | Verification | Status |
|
||||
|--------|----------------|-------|---------|--------------|--------|
|
||||
| PostgreSQL `dev.chunk.embedding` | ~5,000 | Gemma-2-9B (768-dim) | Text semantic search | `SELECT COUNT(embedding) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| Qdrant `momentry_dev_rule1_v2` | ~5,000 points | Gemma-2-9B | Fast vector similarity search | `curl -H "api-key: Test3200Test3200Test3200" "http://localhost:6333/collections/momentry_dev_rule1_v2"` | ⏳ Pending |
|
||||
| Qdrant `_face` collection | ~70,000 points | FaceNet-512 (512-dim) | Face identity search | Face embeddings sync via `sync_face_embeddings()` | ⏳ Pending |
|
||||
|
||||
### Embedding Pipeline
|
||||
1. **Text chunks**: `embeddinggemma_server.py` (port 11436) → 768-dim embedding
|
||||
2. **Face embeddings**: FaceNet CoreML (from face.json) → 512-dim embedding (已在 P0 產生)
|
||||
3. **Sync to Qdrant**: `sync_face_embeddings()` function in Rust
|
||||
|
||||
### Key Points
|
||||
- Text embeddings 使用 Gemma-2-9B (local LLM server)
|
||||
- Face embeddings 使用 FaceNet-512 (CoreML ANE accelerated)
|
||||
- Qdrant 提供 fast similarity search (cosine similarity)
|
||||
|
||||
---
|
||||
|
||||
## P4: Identity Binding
|
||||
|
||||
### Purpose
|
||||
將 detected faces 綁定到 TMDb identities (Audrey Hepburn, Cary Grant, etc.),用於 identity_text search。
|
||||
|
||||
### Identity Matching Pipeline
|
||||
|
||||
| Step | Expected Result | Method | Verification | Status |
|
||||
|------|-----------------|--------|--------------|--------|
|
||||
| TMDb seeds loaded | 23 identities | `tmdb_embed_extractor.py` → TMDb profile face embeddings | `SELECT COUNT(*) FROM dev.identities WHERE source = 'tmdb' AND face_embedding IS NOT NULL` | ✅ Done |
|
||||
| Face matching | ~50,000 bindings | `match_faces_to_tmdb.py` → cosine similarity > 0.5 | `SELECT COUNT(identity_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND identity_id IS NOT NULL` | ⏳ Pending |
|
||||
| Audrey Hepburn faces | ~16,000 | Highest similarity match | `SELECT COUNT(*) FROM dev.face_detections fd JOIN dev.identities i ON fd.identity_id = i.id WHERE fd.file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND i.name = 'Audrey Hepburn'` | ⏳ Pending |
|
||||
| Cary Grant faces | ~5,000 | Second highest match | Same query for Cary Grant | ⏳ Pending |
|
||||
|
||||
### Matching Algorithm
|
||||
```python
|
||||
# match_faces_to_tmdb.py
|
||||
for trace_id in traces:
|
||||
for face_embedding in trace_faces:
|
||||
for tmdb_identity in tmdb_identities:
|
||||
similarity = cosine_similarity(face_embedding, tmdb_identity.face_embedding)
|
||||
if similarity >= 0.5:
|
||||
match trace_id → tmdb_identity
|
||||
```
|
||||
|
||||
### Key Points
|
||||
- TMDb seeds 需要 `face_embedding` (之前已驗證: 23 identities with embeddings)
|
||||
- Face `embedding` 必須非 NULL (之前 store_traced_faces.py bug 修復)
|
||||
- Threshold: 0.5 (可調整)
|
||||
|
||||
---
|
||||
|
||||
## P5: API Endpoints
|
||||
|
||||
### Purpose
|
||||
驗證 API endpoints 可以正確返回 identity_text search results。
|
||||
|
||||
### API Tests
|
||||
|
||||
| Endpoint | Purpose | Expected Response | Test Command | Status |
|
||||
|----------|---------|-------------------|--------------|--------|
|
||||
| `/api/v1/search/identity_text` | Search chunk text → identities | Results with `identity_name`, `trace_id`, `identity_source` | `curl "http://localhost:3003/api/v1/search/identity_text?file_uuid=c3c635e3641da80dde10cc555ffcdda5&q=Regina&limit=5"` | ⏳ Pending |
|
||||
| `/api/v1/identities` | List identities with TMDb | Identity list with `tmdb_id`, `face_embedding` | `curl "http://localhost:3003/api/v1/identities?name=Audrey"` | ⏳ Pending |
|
||||
| `/api/v1/progress/:file_uuid` | Check processing progress | JSON with `status`, `completed_processors` | `curl "http://localhost:3003/api/v1/progress/c3c635e3641da80dde10cc555ffcdda5"` | ⏳ Pending |
|
||||
|
||||
### Expected API Response Example
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"chunk_id": "sentence_123",
|
||||
"start_time": 355.0,
|
||||
"text_content": "Oh, mine's Regina Lampert.",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 169
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points
|
||||
- `identity_text` API 需要 `chunk.start_frame` / `chunk.end_frame` 正確 (之前 bug: frame=0)
|
||||
- `identity_id` 必須非 NULL 才能返回 identity_name
|
||||
|
||||
---
|
||||
|
||||
## P6: Completion Criteria
|
||||
|
||||
### Purpose
|
||||
驗證 pipeline 完整完成,所有 ingestion steps 成功。
|
||||
|
||||
### Final Verification Checklist
|
||||
|
||||
| Criteria | Purpose | Check Command | Expected Result | Status |
|
||||
|----------|---------|---------------|-----------------|--------|
|
||||
| All processor outputs exist | 確認所有 processor JSON 檔案產生 | `ls -la output_dev/c3c635e3641da80dde10cc555ffcdda5.*` | 14+ files with size > 0 | ⏳ Pending |
|
||||
| Job status = completed | 確認 worker 完成 job | `SELECT status FROM dev.monitor_jobs WHERE uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `completed` | ⏳ Pending |
|
||||
| Video status = completed | 確認 video state 更新 | `SELECT status FROM dev.videos WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `completed` | ⏳ Pending |
|
||||
| All chunks have embeddings | 確認 text embeddings 完成 | `SELECT COUNT(*) = COUNT(embedding) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (all chunks have embedding) | ⏳ Pending |
|
||||
| Face traces assigned | 確認 face tracking 完成 | `SELECT COUNT(*) = COUNT(trace_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (all faces have trace_id) | ⏳ Pending |
|
||||
| TMDb matching done | 確認 identity binding 完成 | `SELECT COUNT(identity_id) > 40000 FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (> 40K identity bindings) | ⏳ Pending |
|
||||
| Qdrant synced | 確認 vector search ready | Check Qdrant points count | Points increased by ~5,000 | ⏳ Pending |
|
||||
|
||||
### Success Thresholds
|
||||
- **Face detections**: ~70,000 (169K frames / 3 sample interval)
|
||||
- **Identity bindings**: > 40,000 (60% match rate)
|
||||
- **Chunks with embeddings**: > 4,000 (all chunk types)
|
||||
- **Qdrant points**: > 90,000 (current) → > 95,000 (after Charade)
|
||||
|
||||
---
|
||||
|
||||
## Verification Script
|
||||
|
||||
```bash
|
||||
# Run after completion
|
||||
./scripts/verify_charade_pipeline.sh c3c635e3641da80dde10cc555ffcdda5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- OCR processor failed, skipped
|
||||
- Face detection using SwiftFace (ANE accelerated)
|
||||
- TMDb matching using `scripts/match_faces_to_tmdb.py`
|
||||
- Expected total processing time: ~2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-05-27 | M5Max48 | Initial checklist |
|
||||
@@ -0,0 +1,49 @@
|
||||
# Session Summary: Identity Fixes + WP Proxy Fixes + Data Sync
|
||||
|
||||
**Date**: 2026-05-29
|
||||
**Author**: OpenCode
|
||||
**Status**: Completed (marcom team testing)
|
||||
|
||||
## What Was Done (Chronological)
|
||||
|
||||
### 1. Production Identity Fixes (3002)
|
||||
- **James Coburn restored** (id=18738, confirmed)
|
||||
- **Chantal Goya restored** (id=18737, confirmed)
|
||||
- **Louis Viret name/status fixed**
|
||||
- **Sequences fixed**: `identities_id_seq` (48→18734), `face_detections_id_seq` (141383→932413), `identity_history_id_seq`, `identity_bindings_id_seq`, `pre_chunks_id_seq`, `file_identities_id_seq`
|
||||
- **COALESCE fix** for `reference_data` NULL crash (`postgres_db.rs:3198`, `storage.rs:196`)
|
||||
|
||||
### 2. Bug Fixes
|
||||
- **DELETE identity**: Fixed binding order bug + removed `identity_confidence` column reference
|
||||
- **PATCH identity**: `jsonb_deep_merge` Nested JSON metadata
|
||||
- **mergeinto UNDO/REDO**: MongoDB deserialization fix (`Collection<Document>`)
|
||||
|
||||
### 3. Library Page Infinite Load Fix
|
||||
- **Root cause**: WP scan proxy (snippet 48) didn't forward query params → infinite pagination loop
|
||||
- **Fix**: Added `$request->get_query_params()` forwarding in scan proxy
|
||||
- **Safety**: Added `maxPages = 10` limit in JS pagination
|
||||
|
||||
### 4. Identity Data Sync (Dev → Production)
|
||||
- **Full replacement** of `public.identities`, `public.identity_bindings`, `public.identity_history` with dev data
|
||||
- James Coburn id: 18738 → 11
|
||||
- Bindings: 11,892 → 12,834 (+942)
|
||||
- **Verification**: 0 differences between schemas
|
||||
|
||||
### 5. Snippet 55 Filter
|
||||
- Added `.filter(f => f.is_registered)` to show only registered files on library page
|
||||
- Changed `status:'unregistered'` → `status: f.status || 'unregistered'`
|
||||
|
||||
## Key Decisions
|
||||
- Library page filter: default show registered files only
|
||||
- Identity sync: full DELETE + INSERT (not UPDATE) to ensure consistency
|
||||
- No user-defined metadata fields (starred/notes/role) preserved — matches dev exactly
|
||||
|
||||
## Handoff to Marcom
|
||||
- `/people/` page should show correct identity state
|
||||
- `/library/` page should show only registered files (4 currently)
|
||||
- Login required for `/library/` — redirects to `/login/` if not authenticated
|
||||
|
||||
## Files Modified
|
||||
- `snippet 48` (/scan WP proxy — query param forwarding)
|
||||
- `snippet 55` (library page JS — registered-only filter, maxPages safety)
|
||||
- `docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md` (sync record)
|
||||
45
docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md
Normal file
45
docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Identity Data Sync: Dev (3003) → Production (3002)
|
||||
|
||||
**Date**: 2026-05-29
|
||||
**Author**: OpenCode
|
||||
**Status**: Completed
|
||||
|
||||
## Summary
|
||||
|
||||
Fully synced all identity-related tables from dev schema to public schema on PostgreSQL `momentry` database.
|
||||
|
||||
## What Was Done
|
||||
|
||||
1. **Identities table** (`public.identities`): Replaced with `dev.identities` (69 records, original ids preserved)
|
||||
2. **Identity_bindings** (`public.identity_bindings`): Replaced with `dev.identity_bindings` (12,834 records)
|
||||
3. **Identity_history** (`public.identity_history`): Replaced with `dev.identity_history` (10 records)
|
||||
4. **Sequences**: Updated `identities_id_seq`, `identity_bindings_id_seq`, `identity_history_id_seq` to match
|
||||
|
||||
### Key Changes
|
||||
- **James Coburn**: Changed from id=18738 → id=11 (dev's original id)
|
||||
- **Chantal Goya**: Changed from id=18737 → id=18736 (dev's id)
|
||||
- **Metadata**: Now matches dev schema — TMDB fields only, no user-defined fields (starred, notes, role, aliases, user_confirmed are removed as expected)
|
||||
- **Bindings**: Increased from 11,892 → 12,834 (+942 bindings)
|
||||
|
||||
### Not Changed
|
||||
- `face_detections` — identical in both schemas (135,521 records)
|
||||
- `pre_chunks` — large difference (public: 1.3M vs dev: 3.3M) but NOT related to identity
|
||||
- All other non-identity tables unchanged
|
||||
|
||||
## Verification
|
||||
|
||||
```sql
|
||||
-- Counts match
|
||||
identities: 69 = 69 ✅
|
||||
identity_bindings: 12,834 = 12,834 ✅
|
||||
identity_history: 10 = 10 ✅
|
||||
|
||||
-- No differences
|
||||
id/uuid mismatch: 0
|
||||
metadata/status/name diffs: 0
|
||||
```
|
||||
|
||||
## Files Referenced
|
||||
|
||||
- `AGENTS.md` — Development isolation rules
|
||||
- `/Users/accusys/momentry_core/docs_v1.0/M4_workspace/2026-05-29_wp_api_url_update.md` — Previous session handoff
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-05-29: Mergeinto NULL face_id Fix
|
||||
|
||||
## Problem
|
||||
Production server (3002) returned `"error":"error occurred while decoding column 0: unexpected null; try decoding as an 'Option'"` when using mergeinto after clicking undo on a merge.
|
||||
|
||||
## Root Cause
|
||||
`src/api/identity_binding.rs:428` decodes `face_id` from `face_detections` as `String` (non-Option), but **135,521 records** in the production `face_detections` table have NULL `face_id`. When merging an identity whose face_detections include NULL face_ids, the SQLx decode panics.
|
||||
|
||||
## Fix
|
||||
- Changed `(String, Option<i32>)` → `(Option<String>, Option<i32>)` at line 428
|
||||
- Changed `face_id_list` to use `filter_map` instead of `map` to skip NULL face_ids
|
||||
- Changed `faces_count` to use `face_id_list.len()` instead of `face_ids.len()` (matching the actual transferred count)
|
||||
|
||||
## Files Changed
|
||||
- `momentry_core/src/api/identity_binding.rs` — 3 lines changed
|
||||
|
||||
## Verification
|
||||
- 234 library tests pass
|
||||
- `cargo fmt` passes
|
||||
- Production binary rebuilt (`target/release/momentry`)
|
||||
- Production server restarted on port 3002 (PID 92043)
|
||||
|
||||
## Identities with NULL face_id (20 identities, ~135k records)
|
||||
Audrey Hepburn (36k), Cary Grant (15k), Bernard Musson, Walter Matthau, Jacques Marin, George Kennedy, Michel Thomass, Antonio Passalia, etc. — all `type=people, status=confirmed`. These identities were likely imported from bulk face detection data without face_id generation.
|
||||
|
||||
## Data Note
|
||||
The NULL face_ids are a pre-existing data quality issue. The fix prevents crashes but doesn't clean up the NULL data. Faces with NULL face_id won't be tracked in undo history (they stay with the target after undo), but the bulk transfer (`WHERE identity_id = $1`) still works correctly.
|
||||
68
docs_v1.0/OPERATIONS/TMDb_Pipeline_Test_2026-05-17.md
Normal file
68
docs_v1.0/OPERATIONS/TMDb_Pipeline_Test_2026-05-17.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# TMDb Pipeline Test 2026-05-17
|
||||
|
||||
## Purpose
|
||||
Verify full TMDb enrichment pipeline: register → process → TMDb prefetch → probe → identity files → downloads.
|
||||
|
||||
## Environment
|
||||
- **Server**: playground (port 3003)
|
||||
- **Schema**: `dev`
|
||||
- **TMDB_API_KEY**: `e9cde52197f6f8df4d9db99da93db1fb`
|
||||
- **Build**: `momentry_playground` (debug, 0 errors)
|
||||
|
||||
## Pre-cleanup
|
||||
Unregistered old files + deleted output files:
|
||||
```bash
|
||||
POST /api/v1/unregister {"file_uuid": "3abeee81..."}
|
||||
POST /api/v1.unregister {"file_uuid": "23b1c872..."}
|
||||
```
|
||||
|
||||
## Step 1: Register
|
||||
|
||||
| File | UUID | Result |
|
||||
|------|------|--------|
|
||||
| Charade main | `bd80fec92b0b6963d177a2c55bf713e2` | ✅ Registered (already_exists due to content_hash match) |
|
||||
| Charade YouTube | `a6fb22eebefaef17e62af874997c5944` | ✅ Fresh registration |
|
||||
|
||||
Register phase completed: probe → CUT → scene classification.
|
||||
|
||||
## Step 2: Trigger Processing
|
||||
|
||||
```bash
|
||||
POST /api/v1/file/:uuid/process {}
|
||||
```
|
||||
|
||||
Jobs created:
|
||||
- Main: job_id=167, status=PENDING
|
||||
- YouTube: job_id=168, status=PENDING
|
||||
|
||||
Worker blocked by schema issue: `processor_results` missing `retry_count` column + `jsonb_set(text, text, jsonb)` signature mismatch. Fixed `retry_count` via ALTER TABLE.
|
||||
|
||||
## Step 3: TMDb Prefetch (requires pipeline completion first)
|
||||
|
||||
```bash
|
||||
POST /api/v1/agents/tmdb/prefetch
|
||||
```
|
||||
|
||||
## Step 4: TMDb Probe
|
||||
|
||||
```bash
|
||||
POST /api/v1/file/:uuid/tmdb-probe
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
1. `jsonb_set(jsonb, text, jsonb)` → should be `jsonb_set(jsonb, text[], jsonb)` — pre-existing worker bug
|
||||
2. `processor_results.retry_count` column missing — fixed via ALTER TABLE
|
||||
3. Worker requires running as separate process: `./target/debug/momentry_playground worker`
|
||||
|
||||
## Endpoint Changes in This Test
|
||||
| Endpoint | Status |
|
||||
|----------|--------|
|
||||
| `GET /api/v1/stats/ingest` | ❌ Removed (stats moved to files/scan + identities) |
|
||||
| `GET /api/v1/files/scan` | ➕ Added `total_chunks`, `searchable_chunks`, `pending_videos` |
|
||||
| `GET /api/v1/identities` | ➕ Added `total_identities`, `tmdb_identities`, `auto_identities` |
|
||||
| `POST /api/v1/agents/tmdb/prefetch` | ✅ Writes identity files directly |
|
||||
| `POST /api/v1/file/:uuid/tmdb-probe` | ✅ Upserts from disk identity files |
|
||||
| `GET /api/v1/identity/:uuid/json` | ✅ Download identity JSON |
|
||||
| `GET /api/v1/file/:uuid/json/:processor` | ✅ Download processor JSON |
|
||||
| `POST /api/v1/agents/identity/match-from-photo` | 🆕 New |
|
||||
| `POST /api/v1/agents/identity/match-from-trace` | 🆕 New |
|
||||
375
docs_v1.0/REFERENCE/FACE_BINDING_STATES.md
Normal file
375
docs_v1.0/REFERENCE/FACE_BINDING_STATES.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Face Binding States — Data Model Reference
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Date**: 2026-05-25
|
||||
**Related**: `GET /api/v1/file/:file_uuid/faces`, `identities`, `strangers`, `face_detections`
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **face detection** | A single face bounding box detected in one video frame. Stored in `face_detections` table. |
|
||||
| **trace** | A sequence of face detections belonging to the same person across consecutive frames. Assigned by the face tracker. `trace_id` groups multiple face detections. |
|
||||
| **identity** | A known person with a name. Sources: TMDb (movie stars), user-defined (manual entry). Stored in `identities` table with `source='tmdb'` or `source='user_defined'`. |
|
||||
| **stranger** | An unknown person detected but not matched to any known identity. Created automatically for unmatched traces. Stored in `strangers` table. |
|
||||
| **binding** | The association between a face detection and either an identity or a stranger. Represented by `identity_id` or `stranger_id` FK in `face_detections`. |
|
||||
| **TMDb** | The Movie Database. Source of celebrity identity seeds with `face_embedding` for matching. |
|
||||
| **auto identity** | Legacy term for identities created from `face_clustered.json` analysis. Now migrated to `strangers` table as reference records. |
|
||||
| **dangling** | A face detection whose `identity_id` points to a deleted identity (e.g., auto identities removed during migration). |
|
||||
| **unbound** | A face detection with no binding at all — `identity_id IS NULL AND stranger_id IS NULL`. |
|
||||
| **PK** | Primary Key. A unique identifier for each row in a table. Example: `identities.id`, `strangers.id`, `face_detections.id`. |
|
||||
| **FK** | Foreign Key. A column that references the PK of another table, creating a relationship. Example: `face_detections.identity_id` → `identities.id`, `face_detections.stranger_id` → `strangers.id`. FK ensures referential integrity — a face cannot point to a non-existent identity. |
|
||||
|
||||
---
|
||||
|
||||
## Three Core Tables
|
||||
|
||||
### ER Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ identities │ │ strangers │
|
||||
│─────────────────────│ │─────────────────────│
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ uuid │ │ file_uuid │
|
||||
│ name │ │ trace_id │
|
||||
│ source │ │ metadata │
|
||||
│ tmdb_id │ │ created_at │
|
||||
│ face_embedding │ │ │
|
||||
│ metadata │ │ UNIQUE(file_uuid, │
|
||||
│ status │ │ trace_id) │
|
||||
│ ... │ │ │
|
||||
└─────────┬───────────┘ └─────────┬───────────┘
|
||||
│ │
|
||||
│ FK │ FK
|
||||
│ (ON DELETE SET NULL) │ (ON DELETE SET NULL)
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ face_detections │
|
||||
│─────────────────────────────────────────────────────│
|
||||
│ id (PK) │
|
||||
│ file_uuid — Video file identifier │
|
||||
│ frame_number — Frame where face was detected│
|
||||
│ timestamp_secs — Frame number / fps │
|
||||
│ trace_id — Face tracking ID │
|
||||
│ face_id — Format: `{frame}_{idx}` │
|
||||
│ identity_id (FK) — → identities.id │
|
||||
│ stranger_id (FK) — → strangers.id │
|
||||
│ x, y, width, height — Bounding box │
|
||||
│ confidence — Detection confidence (0–1) │
|
||||
│ embedding — Face embedding vector │
|
||||
│ metadata — JSON metadata │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Table Summary
|
||||
|
||||
| Table | Role | Record Count (public) | Primary Key |
|
||||
|-------|------|----------------------|-------------|
|
||||
| `identities` | Known persons (TMDb, user-defined) | 70 | `id`, `uuid` |
|
||||
| `strangers` | Unknown persons (unmatched traces) | 0–N per file | `id`, `(file_uuid, trace_id)` |
|
||||
| `face_detections` | Individual face detections | 70691 per file | `id` |
|
||||
|
||||
### Key Columns in `face_detections`
|
||||
|
||||
| Column | Type | Purpose |
|
||||
|--------|------|---------|
|
||||
| `identity_id` | INTEGER FK | Points to `identities.id` if matched to known person |
|
||||
| `stranger_id` | INTEGER FK | Points to `strangers.id` if unmatched trace |
|
||||
| `trace_id` | INTEGER | Groups faces belonging to same person across frames |
|
||||
|
||||
**Design Rule**: `identity_id` and `stranger_id` are mutually exclusive in normal operation. A face should have only one binding.
|
||||
|
||||
---
|
||||
|
||||
## Four Binding States
|
||||
|
||||
### State Definitions
|
||||
|
||||
| # | State | `binding` JSON | SQL Condition | Meaning |
|
||||
|---|-------|----------------|---------------|---------|
|
||||
| 1 | **identity** | `{"identity_id": 9, "identity_uuid": "...", "identity_name": "Audrey Hepburn"}` | `identity_id IN (SELECT id FROM identities)` | Face matched to a known TMDb or user-defined identity |
|
||||
| 2 | **stranger** | `{"stranger_id": 845, "metadata": {}}` | `stranger_id IS NOT NULL` | Face belongs to an unmatched trace (unknown person) |
|
||||
| 3 | **dangling** | `{"old_identity_id": 18052}` | `identity_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM identities WHERE id = face_detections.identity_id)` | Face was bound to an identity that has been deleted (orphaned reference) |
|
||||
| 4 | **unbound** | `null` | `identity_id IS NULL AND stranger_id IS NULL` | Face has no binding at all |
|
||||
|
||||
### State Detection Logic (Rust)
|
||||
|
||||
```rust
|
||||
let binding = if let (Some(iid), Some(iuuid), Some(iname)) =
|
||||
(identity_id, identity_uuid, identity_name)
|
||||
{
|
||||
FaceBinding::Identity { identity_id: iid, identity_uuid: iuuid, identity_name: iname }
|
||||
} else if let Some(sid) = stranger_id {
|
||||
FaceBinding::Stranger { stranger_id: sid, metadata: stranger_metadata }
|
||||
} else if let Some(iid) = identity_id {
|
||||
FaceBinding::Dangling { old_identity_id: iid }
|
||||
} else {
|
||||
FaceBinding::Unbound
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
### Processing Pipeline
|
||||
|
||||
```
|
||||
Video Registration
|
||||
│
|
||||
▼
|
||||
Face Detection
|
||||
(face_detections created)
|
||||
│
|
||||
▼
|
||||
Face Tracking
|
||||
(trace_id assigned)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Identity Agent │
|
||||
│ Face Matching │
|
||||
└────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ MATCHED │ │ UNMATCHED│
|
||||
│ to TMDb │ │ trace │
|
||||
└─────┬────┘ └────┬─────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
identity_id=X stranger_id=S
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ IDENTITY│ │ STRANGER│
|
||||
│ state │ │ state │
|
||||
└─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### User Operations
|
||||
|
||||
```
|
||||
┌─────────┐ bind ┌─────────┐
|
||||
│ STRANGER│──────────────▶│ IDENTITY│
|
||||
└────┬────┘ └────┬────┘
|
||||
│ │
|
||||
│ unbind │
|
||||
│ (if stranger_id │
|
||||
│ preserved) │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ STRANGER│◀─────────────│ UNBOUND │
|
||||
│ (rollback) │ (if no │
|
||||
└─────────┘ │ stranger)│
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### Migration Effect
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ auto identities │
|
||||
│ (source='auto') │
|
||||
│ 943 records │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
│ DELETE
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ face_detections │
|
||||
│ identity_id=18052 │
|
||||
│ (points to deleted) │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
│ Cleanup SQL
|
||||
│ SET identity_id=NULL
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ DANGLING → UNBOUND │
|
||||
│ 18641 faces cleaned │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQL Query Examples
|
||||
|
||||
### Count by State
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE identity_id IN (SELECT id FROM identities)) AS identity,
|
||||
COUNT(*) FILTER (WHERE stranger_id IS NOT NULL) AS stranger,
|
||||
COUNT(*) FILTER (WHERE identity_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM identities WHERE id = face_detections.identity_id)) AS dangling,
|
||||
COUNT(*) FILTER (WHERE identity_id IS NULL AND stranger_id IS NULL) AS unbound
|
||||
FROM face_detections
|
||||
WHERE file_uuid = 'aeed71342a899fe4b4c57b7d41bcb692';
|
||||
```
|
||||
|
||||
### Filter by State
|
||||
|
||||
```sql
|
||||
-- Identity
|
||||
SELECT * FROM face_detections fd
|
||||
WHERE fd.identity_id IN (SELECT id FROM identities);
|
||||
|
||||
-- Stranger
|
||||
SELECT * FROM face_detections WHERE stranger_id IS NOT NULL;
|
||||
|
||||
-- Dangling
|
||||
SELECT * FROM face_detections fd
|
||||
WHERE fd.identity_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM identities WHERE id = fd.identity_id);
|
||||
|
||||
-- Unbound
|
||||
SELECT * FROM face_detections
|
||||
WHERE identity_id IS NULL AND stranger_id IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## bind/unbind Behavior
|
||||
|
||||
### Current Implementation (stranger_id cleared on bind)
|
||||
|
||||
| Operation | SQL Effect | Result |
|
||||
|-----------|------------|--------|
|
||||
| `bind_face_to_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `bind_trace_to_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `merge_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `unbind_face` | `SET identity_id=NULL` | Becomes unbound (cannot rollback) |
|
||||
|
||||
**Problem**: After bind → unbind, face becomes unbound instead of returning to stranger.
|
||||
|
||||
### Proposed Fix (preserve stranger_id on bind)
|
||||
|
||||
| Operation | SQL Effect | Result |
|
||||
|-----------|------------|--------|
|
||||
| `bind_face_to_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `bind_trace_to_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `merge_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `unbind_face` | `SET identity_id=NULL` | Returns to stranger (if stranger_id exists) |
|
||||
|
||||
**Change Required**: Remove `, stranger_id = NULL` from three UPDATE queries in `identity_binding.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Why Dangling Happens
|
||||
|
||||
Dangling occurs when `face_detections.identity_id` points to a deleted row in `identities` table.
|
||||
|
||||
### Root Cause
|
||||
|
||||
At the time of migration, `face_detections.identity_id` **had no FK constraint** to `identities.id`. This allowed:
|
||||
|
||||
1. `DELETE FROM identities WHERE source='auto'` succeeded without error
|
||||
2. `face_detections.identity_id` values remained unchanged (pointing to deleted IDs)
|
||||
3. No `ON DELETE SET NULL` triggered because no FK existed
|
||||
|
||||
### Prevention
|
||||
|
||||
With FK constraint in place:
|
||||
```sql
|
||||
ALTER TABLE face_detections
|
||||
ADD CONSTRAINT fk_face_detections_identity
|
||||
FOREIGN KEY (identity_id) REFERENCES identities(id) ON DELETE SET NULL;
|
||||
```
|
||||
|
||||
Deleting an identity would automatically set `face_detections.identity_id = NULL` (no dangling).
|
||||
|
||||
### Current Status
|
||||
|
||||
After migration cleanup:
|
||||
- Public schema: FK `fk_face_detections_stranger` exists (on `stranger_id`)
|
||||
- Public schema: FK `fk_face_detections_identity` **does not exist** (historical reason)
|
||||
- Dev schema: Same state as public
|
||||
|
||||
---
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### `GET /api/v1/file/:file_uuid/faces`
|
||||
|
||||
**Purpose**: List all face detections in a file with binding state.
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `page` | int | 1 | Page number |
|
||||
| `page_size` | int | 50 | Items per page |
|
||||
| `binding` | string | — | Filter: `identity`, `stranger`, `dangling`, `unbound` |
|
||||
| `trace_id` | int | — | Filter by trace ID |
|
||||
| `min_confidence` | float | — | Minimum confidence (0.0–1.0) |
|
||||
| `start_frame` | int | — | Start frame (inclusive) |
|
||||
| `end_frame` | int | — | End frame (inclusive) |
|
||||
|
||||
**Response Example**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"total": 52244,
|
||||
"page": 1,
|
||||
"page_size": 2,
|
||||
"data": [
|
||||
{
|
||||
"id": 661508,
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"frame_number": 21297,
|
||||
"timestamp_secs": 851.88,
|
||||
"face_id": "21297_0",
|
||||
"trace_id": 485,
|
||||
"bbox": { "x": 1072, "y": 390, "width": 56, "height": 56 },
|
||||
"confidence": 0.6114,
|
||||
"binding": {
|
||||
"identity_id": 9,
|
||||
"identity_uuid": "c3545906-c82d-4b66-aa1d-150bc02decce",
|
||||
"identity_name": "Audrey Hepburn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Reference
|
||||
|
||||
### `migrate_strangers_table.sql` (Summary)
|
||||
|
||||
1. `CREATE TABLE strangers`
|
||||
2. Insert unmatched traces → strangers
|
||||
3. Preserve auto identity metadata → strangers (NULL file_uuid/trace_id)
|
||||
4. Update `face_detections.stranger_id` → FK
|
||||
5. Add FK constraint
|
||||
6. Delete legacy `identity_bindings` for auto identities
|
||||
7. Delete `identities` where `source='auto'`
|
||||
8. Cleanup dangling `identity_id` (set to NULL)
|
||||
|
||||
### Cleanup SQL (Dangling)
|
||||
|
||||
```sql
|
||||
UPDATE face_detections fd
|
||||
SET identity_id = NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.id = fd.identity_id)
|
||||
AND fd.identity_id IS NOT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-25*
|
||||
1
docs_v1.0/doc-wasm
Symbolic link
1
docs_v1.0/doc-wasm
Symbolic link
@@ -0,0 +1 @@
|
||||
doc_wasm
|
||||
@@ -38,7 +38,7 @@ a { color: #0066cc; }
|
||||
<h2>Search APIs</h2>
|
||||
<h3><code>POST /api/v1/search/smart</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector <code>story_parent</code> and <code>llm_parent</code> chunks by cosine similarity.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
@@ -53,13 +53,6 @@ a { color: #0066cc; }
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>File UUID to search within</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
@@ -67,6 +60,13 @@ a { color: #0066cc; }
|
||||
<td>Search text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>File UUID to search within. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
@@ -89,7 +89,14 @@ a { color: #0066cc; }
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<h4>Example (Global Search)</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/smart"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"query": "Audrey Hepburn"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/smart"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
@@ -101,6 +108,7 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"results"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"parent_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scene_order"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">104438</span><span class="p">,</span>
|
||||
@@ -118,10 +126,26 @@ a { color: #0066cc; }
|
||||
<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>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where result was found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/universal</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL <code>tsvector</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
@@ -147,7 +171,7 @@ a { color: #0066cc; }
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Restrict to specific file</td>
|
||||
<td>Restrict to specific file. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>types</code></td>
|
||||
@@ -179,7 +203,14 @@ a { color: #0066cc; }
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<h4>Example (Global Search)</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"query": "Cary Grant"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
@@ -191,6 +222,7 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"results"</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">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chunk"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec92b0b6963d177a2c55bf713e2_2"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"story_child"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5103</span><span class="p">,</span>
|
||||
@@ -199,6 +231,25 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">213.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[213s-214s] Cary Grant: \"Olá!\""</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"frame"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"frame_number"</span><span class="p">:</span><span class="w"> </span><span class="mi">5105</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="mf">212.72</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.7</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"objects"</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">"ocr_texts"</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">"faces"</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">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"person"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_id"</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">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"appearance_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">542</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.95</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
@@ -206,16 +257,140 @@ a { color: #0066cc; }
|
||||
<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>results[].type</code></td>
|
||||
<td>string</td>
|
||||
<td>Result type: <code>chunk</code>, <code>frame</code>, or <code>person</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where result was found (all types)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/frames</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Search face detection frames by identity name or trace ID.</p>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/identity_text</code></h3>
|
||||
<h3><code>GET /api/v1/search/identity_text</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Search text chunks spoken by a specific identity.</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>q</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Search text (ILIKE match)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Restrict to specific file. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>50</td>
|
||||
<td>Max results</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>50</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example (Global Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/identity_text?q=love"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/identity_text?file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">&q=love"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</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">"success"</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">"total"</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">"results"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_parent_..._256_270"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">256.256</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">270.228</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text_content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...lack of affection..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_id"</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">"identity_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">94</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>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where chunk was found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].identity_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Identity ID if face was detected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].trace_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Face trace ID</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Visual Search</h3>
|
||||
<table class="table">
|
||||
@@ -282,7 +457,7 @@ a { color: #0066cc; }
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-19 12:49:24</em></p>
|
||||
<p><em>Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -294,6 +294,7 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/thumbnail</code></h3>
|
||||
<p>Extract a single frame from a video as JPEG image. Uses FFmpeg <code>select</code> filter.</p>
|
||||
<p>When <code>frame</code> is omitted, the system automatically selects the best representative frame using the TKG bridge (see algorithm below).</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Query Parameters</h4>
|
||||
@@ -311,9 +312,9 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<tr>
|
||||
<td><code>frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Zero-based frame number to extract</td>
|
||||
<td>No</td>
|
||||
<td>auto-detect</td>
|
||||
<td>Zero-based frame number to extract. Omit for auto-detect.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>x</code></td>
|
||||
@@ -346,8 +347,23 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
</tbody>
|
||||
</table>
|
||||
<p>All four crop params (<code>x</code>, <code>y</code>, <code>w</code>, <code>h</code>) must be provided together or omitted.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Extract frame 1000 (full frame)</span>
|
||||
<h4>Auto-detect Algorithm</h4>
|
||||
<p>When <code>frame</code> is not provided, the endpoint finds the best frame using this fallback chain:</p>
|
||||
<ol>
|
||||
<li><strong>Main characters</strong>: find the two identities with the most face detections (TMDb source)</li>
|
||||
<li><strong>Mutual gaze</strong>: if their face traces have a TKG <code>CO_OCCURS_WITH</code> edge with <code>mutual_gaze=true</code>, take <code>first_frame</code></li>
|
||||
<li><strong>Co-occurrence</strong>: fallback to the first frame where both identities appear together</li>
|
||||
<li><strong>Single identity</strong>: if only one main identity exists, take its highest-quality face frame</li>
|
||||
<li><strong>Any identity</strong>: fallback to the best-quality face frame across all identities</li>
|
||||
<li><strong>Error</strong>: if no face exists, returns <code>404</code></li>
|
||||
</ol>
|
||||
<p>The selected frame is constrained to the <strong>first half of the video</strong> (<code>total_frames / 2</code>).</p>
|
||||
<h4>Examples</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Auto-detect best representative frame</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/thumbnail"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>representative.jpg
|
||||
|
||||
<span class="c1"># Extract frame 1000 (full frame)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>frame_1000.jpg
|
||||
|
||||
@@ -359,10 +375,185 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<h4>Response</h4>
|
||||
<ul>
|
||||
<li><strong>200</strong>: <code>image/jpeg</code> binary data</li>
|
||||
<li><strong>404</strong>: File not found</li>
|
||||
<li><strong>404</strong>: File not found / No faces in file (auto-detect)</li>
|
||||
<li><strong>500</strong>: FFmpeg error (e.g., frame number exceeds video duration)</li>
|
||||
</ul>
|
||||
<h3><code>GET /api/v1/file/:file_uuid/clip</code></h3>
|
||||
<h4>Technical Details</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Backend</strong></td>
|
||||
<td>FFmpeg (<code>ffmpeg-full</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Filter</strong></td>
|
||||
<td><code>select=eq(n\,FRAME)</code> to select frame, optional <code>crop=W:H:X:Y</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Output</strong></td>
|
||||
<td>Single JPEG via pipe (<code>image2pipe</code>, <code>mjpeg</code> codec)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Cache</strong></td>
|
||||
<td><code>Cache-Control: public, max-age=86400</code> (24h)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Frame number</strong></td>
|
||||
<td>Zero-based (<code>frame=0</code> = first frame of video)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/representative-frame</code></h3>
|
||||
<p>Return JSON metadata about the best representative frame for the video. Uses the same auto-detect algorithm as <code>GET /thumbnail</code> (without crop support).</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/representative-frame"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.'</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">"success"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aeed71342a899fe4b4c57b7d41bcb692"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"frame_number"</span><span class="p">:</span><span class="w"> </span><span class="mi">38165</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"timestamp_secs"</span><span class="p">:</span><span class="w"> </span><span class="mf">1526.6</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_quality"</span><span class="p">:</span><span class="w"> </span><span class="mf">37292.97</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"main_identities"</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">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">16456</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2b0ddefe-e2a9-4533-9308-b375594604d5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">10643</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"traces"</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">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">919</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2b0ddefe-e2a9-4533-9308-b375594604d5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"x"</span><span class="p">:</span><span class="w"> </span><span class="mi">764</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"y"</span><span class="p">:</span><span class="w"> </span><span class="mi">237</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">199</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">199</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8426</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"x"</span><span class="p">:</span><span class="w"> </span><span class="mi">1143</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"y"</span><span class="p">:</span><span class="w"> </span><span class="mi">312</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">215</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">215</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8068</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>Response Fields</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>frame_number</code></td>
|
||||
<td>integer</td>
|
||||
<td>Selected representative frame number (primary coordinate)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>timestamp_secs</code></td>
|
||||
<td>float</td>
|
||||
<td>Time in seconds (derived from <code>frame_number / fps</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>face_quality</code></td>
|
||||
<td>float</td>
|
||||
<td>Quality score <code>area × confidence</code> of the best face at this frame</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities</code></td>
|
||||
<td>array</td>
|
||||
<td>Top 2 most frequent TMDb identities in the file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities[].name</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities[].face_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total face detections count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces</code></td>
|
||||
<td>array</td>
|
||||
<td>All face traces present at the selected frame</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].trace_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Face trace ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].identity_uuid</code></td>
|
||||
<td>string or null</td>
|
||||
<td>Matched identity UUID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].name</code></td>
|
||||
<td>string or null</td>
|
||||
<td>Identity name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].x, y, width, height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Bounding box coordinates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].confidence</code></td>
|
||||
<td>float</td>
|
||||
<td>Detection confidence (0.0–1.0)</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>404</code></td>
|
||||
<td>File not found / No faces in file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500</code></td>
|
||||
<td>Database error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Extract a video clip (time range) as MPEG-TS stream. Uses FFmpeg <code>-ss</code> fast seek.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
|
||||
@@ -209,7 +209,191 @@ a { color: #0066cc; }
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-19 12:49:24</em></p>
|
||||
<h2>POST /api/v1/agents/search</h2>
|
||||
<p>Conversational search assistant. Uses Gemma4 function calling to automatically decide which tools to call based on the user's natural language query. Supports multi-turn conversation.</p>
|
||||
<h3>Request</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn 和 Cary Grant 第一次同框在哪個 frame?"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"conversation_id"</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">"file_uuid"</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>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>✅</td>
|
||||
<td>自然語言查詢</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>conversation_id</code></td>
|
||||
<td>string</td>
|
||||
<td>❌</td>
|
||||
<td>延續對話時傳入;新對話不傳</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>❌</td>
|
||||
<td>Portal 有選中檔案時可指定</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Response</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"conversation_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conv_abc123"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"在 Charade (1963) 中,Audrey Hepburn 與 Cary Grant 第一次同框在第 38619 幀(約 1544.76 秒)。"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"need_input"</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">"sources"</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">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tkg_query"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{\"first_cooccurrence\":{\"frame\":38619,\"timestamp_secs\":1544.76}}"</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>conversation_id</code></td>
|
||||
<td>string</td>
|
||||
<td>後續對話需要傳入此 ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>answer</code></td>
|
||||
<td>string</td>
|
||||
<td>Agent 的自然語言回答(或反問)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>need_input</code></td>
|
||||
<td>boolean</td>
|
||||
<td><code>true</code> 表示 agent 需要更多資訊才能回答</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>suggestions</code></td>
|
||||
<td>string[]</td>
|
||||
<td>建議用戶提供的線索(當 <code>need_input=true</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sources</code></td>
|
||||
<td>array</td>
|
||||
<td>引用的工具執行結果</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Conversation Flow</h3>
|
||||
<div class="codehilite"><pre><span></span><code>Round 1: POST /agents/search { query: "我想看男女主角同框" }
|
||||
→ need_input: true, suggestions: ["片名", "演員", "年代"]
|
||||
→ answer: "請問是哪部電影?請提供更多線索"
|
||||
|
||||
Round 2: POST /agents/search { query: "奧黛麗赫本", conversation_id: "..." }
|
||||
→ need_input: false
|
||||
→ answer: "找到 Charade (1963),Audrey Hepburn 和 Cary Grant..."
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Available Tools</h3>
|
||||
<p>Agent 內部使用 Gemma4 function calling 自動調用以下工具:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>find_file</code></td>
|
||||
<td>透過片名/演員/年份關鍵字搜尋影片,回傳 file_uuid + has_data 狀態</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>list_files</code></td>
|
||||
<td>列出近期註冊的影片</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tkg_query</code></td>
|
||||
<td>查詢人物互動資料(7 種子類型:top_identities、first_cooccurrence、identity_details、mutual_gaze、interaction_network、identity_traces、file_info)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>smart_search</code></td>
|
||||
<td>文字內容 ILIKE 搜尋 chunk(可指定 file_uuid 限制範圍)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_identity_detail</code></td>
|
||||
<td>查詢單一身份的詳細資料(角色、TMDb 資訊)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_file_info</code></td>
|
||||
<td>查詢影片基本資訊(片長、解析度)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_representative_frame</code></td>
|
||||
<td>查詢影片最具代表性的 frame 資訊</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Design Principles</h3>
|
||||
<ul>
|
||||
<li><strong>用戶不需要知道 file_uuid</strong> — Agent 會自動用 <code>find_file</code> 搜尋或反問</li>
|
||||
<li><strong>不推薦無資料的影片</strong> — <code>has_data=false</code> 的影片不會被推薦給用戶</li>
|
||||
<li><strong>多輪對話</strong> — 透過 <code>conversation_id</code> 延續上下文,agent 會記得之前的交流</li>
|
||||
<li><strong>並行工具呼叫</strong> — Gemma4 可以一次呼叫多個工具再綜合回答</li>
|
||||
</ul>
|
||||
<h3>Model</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>LLM</strong></td>
|
||||
<td>Gemma4 26B (Q5_K_M)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Engine</strong></td>
|
||||
<td>llama.cpp at <code>localhost:8082</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Endpoint</strong></td>
|
||||
<td><code>/v1/chat/completions</code> (OpenAI-compatible)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Temperature</strong></td>
|
||||
<td>0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Max rounds</strong></td>
|
||||
<td>5 (tool call iterations)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Conversation TTL</strong></td>
|
||||
<td>30 minutes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-22</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
470
docs_v1.0/doc_developer/14_identity_history.html
Normal file
470
docs_v1.0/doc_developer/14_identity_history.html
Normal file
@@ -0,0 +1,470 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>14 Identity History - 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">← 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: identity_history -->
|
||||
<!-- description: Identity PATCH operation history, undo, and redo -->
|
||||
<!-- 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>
|
||||
<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>Full identity record (all fields) before and after each PATCH</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max records</td>
|
||||
<td>256 per identity (oldest auto-deleted when limit exceeded)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Undo steps</td>
|
||||
<td>Unlimited (no expiry, no step limit)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redo stack</td>
|
||||
<td>Cleared on new PATCH (<code>is_undone=true</code> records are deleted)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Stack Model</h4>
|
||||
<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)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
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>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/undo</code></h3>
|
||||
<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>
|
||||
<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 (max records undone in one call)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Behavior</h4>
|
||||
<ul>
|
||||
<li>Queries <code>is_undone=false</code> records, 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>
|
||||
<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">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/undo"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"steps": 1}'</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">"success"</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">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_count"</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">"current_state"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</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">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"people"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confirmed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_id"</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">"tmdb_profile"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</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>undone_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Number of history records undone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>current_state</code></td>
|
||||
<td>object</td>
|
||||
<td>Full identity state after undo</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>400</code></td>
|
||||
<td>No 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 />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/redo</code></h3>
|
||||
<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>
|
||||
<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>
|
||||
<h4>Behavior</h4>
|
||||
<ul>
|
||||
<li>Queries <code>is_undone=true</code> records, 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>
|
||||
<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">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/redo"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"steps": 1}'</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">"success"</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">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"redone_count"</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">"current_state"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</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">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Smith"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"people"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confirmed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"aliases"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="err">...</span><span class="p">]</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_id"</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">"tmdb_profile"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</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>redone_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Number of history records redone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>current_state</code></td>
|
||||
<td>object</td>
|
||||
<td>Full identity state after redo</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>400</code></td>
|
||||
<td>No 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 />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/history</code></h3>
|
||||
<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>
|
||||
<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>
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total"</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">"undo_stack_count"</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">"redo_stack_count"</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">"results"</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">"history_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_undone"</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">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_at"</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">"history_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">41</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_undone"</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">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T11:30:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T13:00:00Z"</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 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>"update"</code> for PATCH)</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 PATCH 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>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</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&limit=10"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Responses</h4>
|
||||
<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>Comparison: PATCH Undo vs Merge Undo</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aspect</th>
|
||||
<th>PATCH Undo/Redo</th>
|
||||
<th>Merge Undo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Storage</td>
|
||||
<td>PostgreSQL <code>identity_history</code></td>
|
||||
<td>MongoDB <code>identity_merge_history</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Trigger</td>
|
||||
<td>Every PATCH</td>
|
||||
<td>Every mergeinto with <code>keep_history=true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Undo deadline</td>
|
||||
<td>None (unlimited)</td>
|
||||
<td>24 hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redo support</td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Step undo</td>
|
||||
<td>Yes (<code>steps</code> param)</td>
|
||||
<td>No (full undo only)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max records</td>
|
||||
<td>256 per identity</td>
|
||||
<td>Unlimited</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-28</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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></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></table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
358
docs_v1.0/doc_user/API_ACCESS.html
Normal file
358
docs_v1.0/doc_user/API_ACCESS.html
Normal file
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Access - 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<h1>Momentry Core API 存取指南</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>版本</td>
|
||||
<td>V1.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>日期</td>
|
||||
<td>2026-03-25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>用途</td>
|
||||
<td>API 存取方式、端點與整合指南</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>版本歷史</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>日期</th>
|
||||
<th>目的</th>
|
||||
<th>操作人</th>
|
||||
<th>工具/模型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>V1.3</td>
|
||||
<td>2026-03-25</td>
|
||||
<td>更新: n8n 搜尋回傳 <code>file_path</code> 取代 <code>media_url</code>,新增 API Key 驗證說明</td>
|
||||
<td>OpenCode</td>
|
||||
<td>deepseek-reasoner</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>V1.2</td>
|
||||
<td>2026-03-24</td>
|
||||
<td>更新網址與服務列表</td>
|
||||
<td>Warren</td>
|
||||
<td>OpenCode / MiniMax M2.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>V1.1</td>
|
||||
<td>2026-03-23</td>
|
||||
<td>初始版本</td>
|
||||
<td>Warren</td>
|
||||
<td>OpenCode / MiniMax M2.5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>基本網址</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>環境</th>
|
||||
<th>URL</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>本地開發</strong></td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>直接訪問 API,繞過反向代理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>外部訪問</strong></td>
|
||||
<td><code>https://m5api.momentry.ddns.net</code></td>
|
||||
<td>通過 Caddy 反向代理訪問,需網路可達</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>何時使用哪個 URL</h3>
|
||||
<p><strong>使用 <code>localhost:3002</code>:</strong>
|
||||
- 開發/測試環境
|
||||
- 直接在伺服器上操作
|
||||
- 當反向代理有問題時</p>
|
||||
<p><strong>使用 <code>m5api.momentry.ddns.net</code>:</strong>
|
||||
- n8n workflow 中呼叫 API
|
||||
- 外部系統整合
|
||||
- 生產環境</p>
|
||||
<h2>認證</h2>
|
||||
<p>所有 <code>/api/v1/*</code> 端點(除了健康檢查 <code>/health</code> 與 <code>/health/detailed</code>)都需要 API Key 認證。</p>
|
||||
<p>請在請求標頭中加入:</p>
|
||||
<div class="codehilite"><pre><span></span><code>X-API-Key: YOUR_API_KEY
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>目前示範使用的 API Key</strong>: <code>demo_api_key_12345</code></p>
|
||||
<blockquote>
|
||||
<p><strong>注意</strong>: 正式環境請使用安全的 API Key 管理機制,避免在客戶端暴露 API Key。</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>影片搜尋 API</h2>
|
||||
<h3>語意搜尋</h3>
|
||||
<p><strong>端點:</strong> <code>POST /api/v1/search</code></p>
|
||||
<p><strong>請求:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"limit"</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">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>欄位</th>
|
||||
<th>類型</th>
|
||||
<th>必填</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>字串</td>
|
||||
<td>是</td>
|
||||
<td>搜尋文字</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>整數</td>
|
||||
<td>否</td>
|
||||
<td>最大回傳結果數(預設 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>uuid</code></td>
|
||||
<td>字串</td>
|
||||
<td>否</td>
|
||||
<td>依影片 UUID 過濾</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>回應:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"results"</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">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">48.8</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">55.44</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fun plot twists, Woody Dialog and charming performances..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.526</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>n8n 整合搜尋</h3>
|
||||
<p><strong>端點:</strong> <code>POST /api/v1/n8n/search</code></p>
|
||||
<p><strong>請求:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>回應:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"count"</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">"hits"</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">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"vid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start"</span><span class="p">:</span><span class="w"> </span><span class="mf">48.8</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end"</span><span class="p">:</span><span class="w"> </span><span class="mf">55.44</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Chunk sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fun plot twists..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.526</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>注意</strong>: API 現在返回 <code>file_path</code>(檔案系統路徑)而非 <code>media_url</code>(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>影片管理 API</h2>
|
||||
<h3>列出所有影片</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/videos</code></p>
|
||||
<h3>查詢影片資訊</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/lookup?uuid={uuid}</code> 或 <code>GET /api/v1/lookup?path={path}</code></p>
|
||||
<h3>取得處理進度</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/progress/{uuid}</code></p>
|
||||
<hr />
|
||||
<h2>區塊資料結構</h2>
|
||||
<p>每個搜尋結果包含影片播放的時間資訊:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>欄位</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>uuid</code></td>
|
||||
<td>影片識別碼</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chunk_id</code></td>
|
||||
<td>區塊唯一識別碼</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chunk_type</code></td>
|
||||
<td>類型:<code>sentence</code>、<code>cut</code>、<code>time_based</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>start_time</code></td>
|
||||
<td>開始時間(秒)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_time</code></td>
|
||||
<td>結束時間(秒)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>text</code></td>
|
||||
<td>語音轉文字內容</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>score</code></td>
|
||||
<td>相關性分數(0-1)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>整合範例</h2>
|
||||
<h3>JavaScript/fetch</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://localhost:3002/api/v1/search'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s1">'POST'</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nx">headers</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span>
|
||||
<span class="w"> </span><span class="s1">'Content-Type'</span><span class="o">:</span><span class="w"> </span><span class="s1">'application/json'</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s1">'X-API-Key'</span><span class="o">:</span><span class="w"> </span><span class="s1">'YOUR_API_KEY'</span><span class="w"> </span><span class="c1">// 替換為實際的 API Key</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="w"> </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="s1">'charade'</span><span class="p">,</span><span class="w"> </span><span class="nx">limit</span><span class="o">:</span><span class="w"> </span><span class="mf">5</span><span class="w"> </span><span class="p">})</span>
|
||||
<span class="p">});</span>
|
||||
<span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
|
||||
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">results</span><span class="p">);</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>PHP/cURL</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="x">$ch = curl_init('http://localhost:3002/api/v1/search');</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_POST, true);</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([</span>
|
||||
<span class="x"> 'query' => 'charade',</span>
|
||||
<span class="x"> 'limit' => 5</span>
|
||||
<span class="x">]));</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_HTTPHEADER, [</span>
|
||||
<span class="x"> 'Content-Type: application/json',</span>
|
||||
<span class="x"> 'X-API-Key: YOUR_API_KEY' // 替換為實際的 API Key</span>
|
||||
<span class="x">]);</span>
|
||||
<span class="x">$response = curl_exec($ch);</span>
|
||||
<span class="x">$data = json_decode($response, true);</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>影片嵌入網址</h2>
|
||||
<blockquote>
|
||||
<p><strong>重要</strong>: API 現在返回 <code>file_path</code>(檔案系統路徑),而非直接可訪問的網址。您需要將檔案路徑轉換為 SFTPGo 分享連結才能嵌入影片。</p>
|
||||
</blockquote>
|
||||
<p><strong>檔案路徑轉換為網址:</strong>
|
||||
- API 返回的 <code>file_path</code> 範例:<code>/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4</code>
|
||||
- 對應的 SFTPGo 分享連結:<code>https://wp.momentry.ddns.net/demo/video.mp4</code>
|
||||
- 轉換方式:移除 <code>/Users/accusys/momentry/var/sftpgo/data/</code> 前綴,將剩餘路徑附加到 <code>https://wp.momentry.ddns.net/</code></p>
|
||||
<p><strong>手動建立分享連結:</strong>
|
||||
1. 開啟 SFTPGo Web UI:<code>http://localhost:8080</code>
|
||||
2. 使用帳號 <code>demo</code> / 密碼 <code>demopassword123</code> 登入
|
||||
3. 導航至 <code>Files</code> → 選擇影片檔案
|
||||
4. 點擊 <code>Share</code> → <code>Create Link</code>
|
||||
5. 複製產生的分享連結</p>
|
||||
<p>使用搜尋結果中的 <code>start_time</code> 和 <code>end_time</code> 來嵌入影片片段。</p>
|
||||
<hr />
|
||||
<h2>服務列表</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>服務</th>
|
||||
<th>網址</th>
|
||||
<th>用途</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Momentry API</td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>核心 API</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SFTPGo</td>
|
||||
<td><code>http://localhost:8080</code></td>
|
||||
<td>檔案儲存</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qdrant</td>
|
||||
<td><code>http://localhost:6333</code></td>
|
||||
<td>向量搜尋</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL</td>
|
||||
<td><code>localhost:5432</code></td>
|
||||
<td>資料庫</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>示範影片</h2>
|
||||
<ul>
|
||||
<li><strong>檔案:</strong> <code>Old_Time_Movie_Show_-_Charade_1963.HD.mov</code></li>
|
||||
<li><strong>UUID:</strong> <code>a1b10138a6bbb0cd</code></li>
|
||||
<li><strong>長度:</strong> 約 6879 秒(約 1.9 小時)</li>
|
||||
<li><strong>區塊數:</strong> 3886 個(句子 + 場景 + 時間)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
3537
docs_v1.0/doc_user/API_ENDPOINTS.html
Normal file
3537
docs_v1.0/doc_user/API_ENDPOINTS.html
Normal file
File diff suppressed because it is too large
Load Diff
207
docs_v1.0/doc_user/API_ERROR_CODES.html
Normal file
207
docs_v1.0/doc_user/API_ERROR_CODES.html
Normal file
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Error Codes - 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "api_reference"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "API Error Codes (API 標準錯誤碼)"
|
||||
date: "2026-05-17"
|
||||
version: "V1.1"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>API Error Codes (API 標準錯誤碼)</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Error Response Format</h2>
|
||||
<p>All API errors follow this JSON structure:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"E001_NOT_FOUND"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Resource not found"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"details"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"file_uuid"</span><span class="p">,</span><span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"abc"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Error Code List</h2>
|
||||
<h3>Generic Errors (E0xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E001_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Resource not found (file, identity, chunk)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E002_DUPLICATE</code></td>
|
||||
<td>409</td>
|
||||
<td>Resource already exists</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E003_VALIDATION</code></td>
|
||||
<td>400</td>
|
||||
<td>Request parameter validation failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E004_UNAUTHORIZED</code></td>
|
||||
<td>401</td>
|
||||
<td>Invalid API key or token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E005_INTERNAL</code></td>
|
||||
<td>500</td>
|
||||
<td>Internal server error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Processor Errors (E1xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E101_PROCESSOR_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Python script execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E102_TIMEOUT</code></td>
|
||||
<td>504</td>
|
||||
<td>Processing timeout</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E103_RESUME_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Resume failed (checkpoint not found)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E104_NO_VIDEO</code></td>
|
||||
<td>400</td>
|
||||
<td>Video file path not found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Identity Errors (E2xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E201_FACE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Face detection not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E202_MERGE_CONFLICT</code></td>
|
||||
<td>409</td>
|
||||
<td>Identity merge conflict</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E203_CANDIDATE_EMPTY</code></td>
|
||||
<td>404</td>
|
||||
<td>No candidates available for confirmation</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>TMDb Errors (E3xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E301_TMDB_NO_KEY</code></td>
|
||||
<td>400</td>
|
||||
<td><code>TMDB_API_KEY</code> environment variable not set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E302_TMDB_UNREACHABLE</code></td>
|
||||
<td>502</td>
|
||||
<td>TMDb API unreachable or timed out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E303_TMDB_CACHE_NOT_FOUND</code></td>
|
||||
<td>200</td>
|
||||
<td>No local TMDb cache; run prefetch first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E304_TMDB_PROBE_FAILED</code></td>
|
||||
<td>500</td>
|
||||
<td>TMDb probe execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E305_TMDB_MOVIE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>No matching TMDb movie found from filename</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
125
docs_v1.0/doc_user/API_INDEX.html
Normal file
125
docs_v1.0/doc_user/API_INDEX.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Index - 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "api_reference"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Momentry Core API 文件總覽"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>Momentry Core API 文件總覽</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>📁 文件結構</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="n">API_WORKSPACE</span><span class="o">/</span>
|
||||
<span class="err">└──</span><span class="w"> </span><span class="n">modules</span><span class="o">/</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="n">_template</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">One</span><span class="o">-</span><span class="n">line</span><span class="w"> </span><span class="n">description</span><span class="w"> </span><span class="n">of</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="n">this</span><span class="w"> </span><span class="k">module</span><span class="w"> </span><span class="n">covers</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">01</span><span class="n">_auth</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Authentication</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">login</span><span class="p">,</span><span class="w"> </span><span class="n">logout</span><span class="p">,</span><span class="w"> </span><span class="n">JWT</span><span class="p">,</span><span class="w"> </span><span class="n">session</span><span class="w"> </span><span class="n">cookie</span><span class="p">,</span><span class="w"> </span><span class="n">API</span><span class="w"> </span><span class="n">key</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">02</span><span class="n">_health</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Health</span><span class="w"> </span><span class="n">check</span><span class="w"> </span><span class="n">endpoints</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">03</span><span class="n">_register</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">File</span><span class="w"> </span><span class="n">registration</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">register</span><span class="p">,</span><span class="w"> </span><span class="n">scan</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">04</span><span class="n">_lookup</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">File</span><span class="w"> </span><span class="n">lookup</span><span class="w"> </span><span class="n">by</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">unregistration</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">05</span><span class="n">_process</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Processing</span><span class="w"> </span><span class="n">pipeline</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">trigger</span><span class="p">,</span><span class="w"> </span><span class="n">probe</span><span class="p">,</span><span class="w"> </span><span class="n">progress</span><span class="p">,</span><span class="w"> </span><span class="n">jobs</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">06</span><span class="n">_search</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Vector</span><span class="w"> </span><span class="n">search</span><span class="p">,</span><span class="w"> </span><span class="n">hybrid</span><span class="w"> </span><span class="n">search</span><span class="p">,</span><span class="w"> </span><span class="n">BM25</span><span class="p">,</span><span class="w"> </span><span class="n">n8n</span><span class="p">,</span><span class="w"> </span><span class="n">visual</span><span class="p">,</span><span class="w"> </span><span class="n">identity</span><span class="w"> </span><span class="n">text</span><span class="w"> </span><span class="n">search</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">07</span><span class="n">_identity</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Global</span><span class="w"> </span><span class="n">identities</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">CRUD</span><span class="p">,</span><span class="w"> </span><span class="n">detail</span><span class="p">,</span><span class="w"> </span><span class="n">files</span><span class="p">,</span><span class="w"> </span><span class="n">faces</span><span class="p">,</span><span class="w"> </span><span class="n">bind</span><span class="p">,</span><span class="w"> </span><span class="n">unbind</span><span class="p">,</span><span class="w"> </span><span class="n">search</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">08</span><span class="n">_identity_agent</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Identity</span><span class="w"> </span><span class="n">agent</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">analyze</span><span class="p">,</span><span class="w"> </span><span class="n">suggest</span><span class="p">,</span><span class="w"> </span><span class="n">merge</span><span class="p">,</span><span class="w"> </span><span class="n">clustering</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">08</span><span class="n">_media</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Video</span><span class="w"> </span><span class="n">streaming</span><span class="w"> </span><span class="o">&</span><span class="w"> </span><span class="n">frame</span><span class="w"> </span><span class="n">extraction</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">09</span><span class="n">_tmdb</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">TMDb</span><span class="w"> </span><span class="n">enrichment</span><span class="w"> </span><span class="n">endpoints</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">prefetch</span><span class="p">,</span><span class="w"> </span><span class="n">probe</span><span class="p">,</span><span class="w"> </span><span class="n">resource</span><span class="p">,</span><span class="w"> </span><span class="n">check</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">10</span><span class="n">_pipeline</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Stats</span><span class="w"> </span><span class="n">endpoints</span><span class="p">,</span><span class="w"> </span><span class="n">inference</span><span class="w"> </span><span class="n">health</span><span class="p">,</span><span class="w"> </span><span class="n">stfpgo</span><span class="w"> </span><span class="n">status</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">11</span><span class="n">_error_codes</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Standard</span><span class="w"> </span><span class="n">API</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="n">codes</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">12</span><span class="n">_agent</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span>
|
||||
<span class="err">└──</span><span class="w"> </span><span class="p">(</span><span class="n">generated</span><span class="w"> </span><span class="n">files</span><span class="w"> </span><span class="err">→</span><span class="w"> </span><span class="n">GUIDES</span><span class="o">/</span><span class="p">)</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>快速選擇指南</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>需求</th>
|
||||
<th>閱讀文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>查看所有 API 端點(curl 範例版)</td>
|
||||
<td><code>GUIDES/API_ENDPOINTS.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>查看快速端點摘要</td>
|
||||
<td><code>GUIDES/API_QUICK_REFERENCE.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>執行 TMDb Enrichment</td>
|
||||
<td><code>GUIDES/TMDb_User_Guide.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>查看錯誤碼</td>
|
||||
<td><code>GUIDES/API_ERROR_CODES.md</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>文件模組清單</h2>
|
||||
<ul>
|
||||
<li><code>_template</code> — One-line description of what this module covers</li>
|
||||
<li><code>01_auth</code> — Authentication — login, logout, JWT, session cookie, API key</li>
|
||||
<li><code>02_health</code> — Health check endpoints</li>
|
||||
<li><code>03_register</code> — File registration — register, scan</li>
|
||||
<li><code>04_lookup</code> — File lookup by name and unregistration</li>
|
||||
<li><code>05_process</code> — Processing pipeline — trigger, probe, progress, jobs</li>
|
||||
<li><code>06_search</code> — Vector search, hybrid search, BM25, n8n, visual, identity text search</li>
|
||||
<li><code>07_identity</code> — Global identities — CRUD, detail, files, faces, bind, unbind, search</li>
|
||||
<li><code>08_identity_agent</code> — Identity agent — analyze, suggest, merge, clustering</li>
|
||||
<li><code>08_media</code> — Video streaming & frame extraction</li>
|
||||
<li><code>09_tmdb</code> — TMDb enrichment endpoints — prefetch, probe, resource, check</li>
|
||||
<li><code>10_pipeline</code> — Stats endpoints, inference health, stfpgo status</li>
|
||||
<li><code>11_error_codes</code> — Standard API error codes</li>
|
||||
<li><code>12_agent</code> — </li>
|
||||
</ul>
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2105
docs_v1.0/doc_user/API_QUICK_REFERENCE.html
Normal file
2105
docs_v1.0/doc_user/API_QUICK_REFERENCE.html
Normal file
File diff suppressed because it is too large
Load Diff
3684
docs_v1.0/doc_user/API_REFERENCE.html
Normal file
3684
docs_v1.0/doc_user/API_REFERENCE.html
Normal file
File diff suppressed because it is too large
Load Diff
1603
docs_v1.0/doc_user/API_TRAINING_MARCOM.html
Normal file
1603
docs_v1.0/doc_user/API_TRAINING_MARCOM.html
Normal file
File diff suppressed because it is too large
Load Diff
1084
docs_v1.0/doc_user/Demo_EndToEnd.html
Normal file
1084
docs_v1.0/doc_user/Demo_EndToEnd.html
Normal file
File diff suppressed because it is too large
Load Diff
472
docs_v1.0/doc_user/M5API_Pipeline_Demo.html
Normal file
472
docs_v1.0/doc_user/M5API_Pipeline_Demo.html
Normal file
@@ -0,0 +1,472 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>M5Api Pipeline Demo - 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "demo_guide"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "M5API Pipeline Demo"
|
||||
date: "2026-05-16"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"
|
||||
tags:
|
||||
- "demo"
|
||||
- "pipeline"
|
||||
- "api"
|
||||
- "m5api"
|
||||
ai_query_hints:
|
||||
- "M5API Pipeline demo"
|
||||
- "如何透過 M5 的 API 執行 Pipeline"
|
||||
related_documents:
|
||||
- "GUIDES/Demo_EndToEnd.md"
|
||||
- "GUIDES/API_ENDPOINTS.md"</p>
|
||||
<hr />
|
||||
<h1>Momentry Core — M5API Pipeline Demo</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>建立者</td>
|
||||
<td>OpenCode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>建立時間</td>
|
||||
<td>2026-05-16</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件版本</td>
|
||||
<td>V1.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key、M5 服務已啟動</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Prerequisites</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">API</span><span class="o">=</span><span class="s2">"https://m5api.momentry.ddns.net"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 1: System Health Check</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{ip, port, status, version, build_git_hash}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.110.201"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3002</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"build_git_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c41f7e0c"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>All core services verified:</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health/detailed"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> services, schema: .schema.ok,</span>
|
||||
<span class="s1"> scripts: .pipeline.scripts_count,</span>
|
||||
<span class="s1"> integrity: .pipeline.scripts_integrity,</span>
|
||||
<span class="s1"> procs: [.pipeline.processors | to_entries[] | select(.value==true and .key!="total_py_files") | .key]</span>
|
||||
<span class="s1">}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"services"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"postgres"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"redis"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"qdrant"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"mongodb"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"schema"</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">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="mi">286</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"integrity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"matched"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"ok"</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">"procs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"asr"</span><span class="p">,</span><span class="s2">"yolo"</span><span class="p">,</span><span class="s2">"face"</span><span class="p">,</span><span class="s2">"pose"</span><span class="p">,</span><span class="s2">"ocr"</span><span class="p">,</span><span class="s2">"cut"</span><span class="p">,</span><span class="s2">"caption"</span><span class="p">,</span><span class="s2">"scene"</span><span class="p">,</span><span class="s2">"story"</span><span class="p">,</span><span class="s2">"asrx"</span><span class="p">,</span><span class="s2">"probe"</span><span class="p">,</span><span class="s2">"visual_chunk"</span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 2: List Registered Files</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files?page=1&page_size=5"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, files: [.data[]? | {name: .file_name[0:50], status}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">56</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ExaSAN PCIe series - Director Ou Yu-Zhi ..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Old_Time_Movie_Show_-_Charade_1963.HD.mov"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Old Felix the Cat Cartoon.mp4"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unregistered"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"short_clip.mov"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 3: Register a New File</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># POST with file_path (must exist on server filesystem)</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/video.mp4"}'</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{success, file_uuid, file_name, file_type, duration, fps, already_exists}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (new registration):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">6785.014</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (duplicate content — SHA256 dedup):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"already_exists"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Content already registered (identical file)"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 4: Probe (ffprobe Metadata)</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">UUID</span><span class="o">=</span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span>
|
||||
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/probe"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{name: .file_name, video: "\(.width)x\(.height)", fps, duration, cached, streams: [.streams[] | {type: .codec_type, codec: .codec_name}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"video"</span><span class="p">:</span><span class="w"> </span><span class="s2">"720x304"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">6785.014</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"cached"</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">"streams"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span><span class="w"> </span><span class="nt">"codec"</span><span class="p">:</span><span class="w"> </span><span class="s2">"h264"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"audio"</span><span class="p">,</span><span class="w"> </span><span class="nt">"codec"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aac"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Error cases:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Non-existent UUID</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"https://m5api.momentry.ddns.net/api/v1/file/bad_uuid/probe"</span>
|
||||
<span class="c1"># → {"error":"Video not found","file_uuid":"bad_uuid"} HTTP 404</span>
|
||||
|
||||
<span class="c1"># File deleted from disk</span>
|
||||
<span class="c1"># → {"error":"File does not exist at registered path","file_uuid":"...","file_path":"..."} HTTP 404</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 5: Submit Processing Job</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Specific processors</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"processors":["asr","cut","yolo","face","pose","ocr"]}'</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/process"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{job_id, file_uuid: .file_uuid[0:16], status}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">167</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d9459762"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PENDING"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>All processors</strong>: Send <code>{}</code> (empty body) to run all 12 processors.
|
||||
Available: <code>asr</code>, <code>cut</code>, <code>yolo</code>, <code>face</code>, <code>pose</code>, <code>ocr</code>, <code>asrx</code>, <code>visual_chunk</code>, <code>scene</code>, <code>story</code>, <code>caption</code></p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>Step 6: Monitor Progress</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="k">while</span><span class="w"> </span>true<span class="p">;</span><span class="w"> </span><span class="k">do</span>
|
||||
<span class="w"> </span><span class="nv">PROGRESS</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">STATUS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.status // "?"'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">PROCS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="k">$(</span>date<span class="w"> </span>+%H:%M:%S<span class="k">)</span><span class="s2">: </span><span class="nv">$PROCS</span><span class="s2">"</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROCS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-q<span class="w"> </span><span class="s2">"completed"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="k">break</span>
|
||||
<span class="w"> </span>sleep<span class="w"> </span><span class="m">10</span>
|
||||
<span class="k">done</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Typical output:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">01</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">11</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">21</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">31</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">41</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">51</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">31</span><span class="p">:</span><span class="mf">01</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Status transition chain</strong>: <code>pending → running → completed</code></p>
|
||||
<p>Check job state:</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/jobs?uuid=</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.jobs[]? | {id, status}]'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 7: Verify Results</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{processors: [.processors[] | {name, status, frames: .frames_processed}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"processors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"asr"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cut"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"yolo"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"face"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pose"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ocr"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 8: Universal Search</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Search for a person name</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"Audrey\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, hits: [.results[]? | {chunk_id: .chunk_id[0:40], text: .text[0:80], score}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</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">"hits"</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">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998193"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Search Chinese text</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"導演\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Search modes</strong>: The universal search endpoint supports:
|
||||
- Text match (ILIKE on <code>text_content</code> and <code>content</code> columns)
|
||||
- Time range filtering (<code>time_range: [start, end]</code>)
|
||||
- Speaker/person ID filtering
|
||||
- Chunk type filtering
|
||||
- Visual content filtering (objects, density, classes)</p>
|
||||
<hr />
|
||||
<h2>Step 9: Get Chunk Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">CHUNK_ID</span><span class="o">=</span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span>
|
||||
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/chunk/</span><span class="si">${</span><span class="nv">CHUNK_ID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{chunk_id, chunk_type, text: .text_content, fps, start_frame, end_frame}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">2395281</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">2395341</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 10: Chunk Fallback (Stale Qdrant Compatibility)</h2>
|
||||
<p>Old integer-format chunk_ids from stale Qdrant payloads are automatically resolved via <code>WHERE id = int(chunk_id)</code>:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Integer format (old Qdrant payload)</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/chunk/998192"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{chunk_id, text: .text_content}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (same chunk as above):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Both formats work:</strong>
|
||||
- <code>chunk/{uuid}_{id}</code> → exact <code>chunk_id</code> match
|
||||
- <code>chunk/{id}</code> → fallback by primary key <code>id</code></p>
|
||||
<hr />
|
||||
<h2>Step 11: File Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{file_name, status, file_type, file_path}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/accusys/momentry/var/sftpgo/data/demo/Charade..."</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 12: File Identities</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/identities"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, identities: [.data[]? | {name, face_count, confidence}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</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">"identities"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span><span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">22082</span><span class="p">,</span><span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.93</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span><span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">15334</span><span class="p">,</span><span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.91</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 13: Identity Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># List all global identities</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identities?page=1&page_size=3"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, identities: [.data[]? | {name, type: .identity_type, source}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Get identity files (cross-file faces)</span>
|
||||
<span class="nv">IDENTITY_UUID</span><span class="o">=</span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="si">${</span><span class="nv">IDENTITY_UUID</span><span class="si">}</span><span class="s2">/files"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, files: [.data[]? | {file_uuid: .file_uuid[0:16], face_count}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 14: Schema & Integrity Verification</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health/detailed"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> ip, port,</span>
|
||||
<span class="s1"> schema: .schema.ok,</span>
|
||||
<span class="s1"> migrations: [.schema.applied[]?.filename],</span>
|
||||
<span class="s1"> integrity: .pipeline.scripts_integrity</span>
|
||||
<span class="s1">}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.110.201"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3002</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"schema"</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">"migrations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_content_hash.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_registered_status.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_schema_version.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_cleanup_inactive_identities.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_schema_v4_tables.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_schema_v4.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_v4_complete.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_fix_chunk_id_format.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_identity_indexes.sql"</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"integrity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"matched"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Full Automation Script</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="ch">#!/bin/bash</span>
|
||||
<span class="nb">set</span><span class="w"> </span>-euo<span class="w"> </span>pipefail
|
||||
|
||||
<span class="nv">API</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">API</span><span class="k">:-</span><span class="nv">https</span><span class="p">://m5api.momentry.ddns.net</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">KEY</span><span class="k">:-</span><span class="nv">muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69</span><span class="si">}</span><span class="s2">"</span>
|
||||
|
||||
<span class="c1"># 1. Health</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Health ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{status, version, build_git_hash}'</span>
|
||||
|
||||
<span class="c1"># 2. Register file (argument: file path)</span>
|
||||
<span class="nv">FILE_PATH</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">1</span><span class="p">:?Usage: </span><span class="nv">$0</span><span class="p"> <file_path></span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Register ==="</span>
|
||||
<span class="nv">REG</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"file_path\":\"</span><span class="nv">$FILE_PATH</span><span class="s2">\"}"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="k">)</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$REG</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{success, file_uuid, file_name}'</span>
|
||||
<span class="nv">UUID</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$REG</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.file_uuid'</span><span class="k">)</span>
|
||||
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="nv">$UUID</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Registration failed"</span><span class="p">;</span><span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
|
||||
<span class="c1"># 3. Probe</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Probe ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/probe"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{name, fps, duration}'</span>
|
||||
|
||||
<span class="c1"># 4. Submit job</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Process ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{}'</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/process"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{job_id, status}'</span>
|
||||
|
||||
<span class="c1"># 5. Poll progress</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Waiting for pipeline... ==="</span>
|
||||
<span class="k">while</span><span class="w"> </span>true<span class="p">;</span><span class="w"> </span><span class="k">do</span>
|
||||
<span class="w"> </span><span class="nv">PROGRESS</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">STATUS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.status // "?"'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="k">$(</span>date<span class="w"> </span>+%H:%M:%S<span class="k">)</span><span class="s2">: </span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")'</span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-e<span class="w"> </span><span class="s1">'[.processors[]? | select(.status == "pending")] | length == 0'</span><span class="w"> </span>>/dev/null<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="k">break</span>
|
||||
<span class="w"> </span>sleep<span class="w"> </span><span class="m">10</span>
|
||||
<span class="k">done</span>
|
||||
|
||||
<span class="c1"># 6. Search</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Search ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"test\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, hits: [.results[]? | {chunk_id: .chunk_id[0:30], text: .text[0:60]}]}'</span>
|
||||
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"✅ Done: </span><span class="nv">$UUID</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
923
docs_v1.0/doc_user/TMDb_User_Guide.html
Normal file
923
docs_v1.0/doc_user/TMDb_User_Guide.html
Normal file
@@ -0,0 +1,923 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tmdb User Guide - 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "user_manual"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "TMDb Enrichment 使用指南"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>TMDb Enrichment 使用指南</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Base URL</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment</th>
|
||||
<th>URL</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Playground (Dev)</td>
|
||||
<td><code>http://localhost:3003</code></td>
|
||||
<td>Development and testing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Production</td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>Production deployment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>External (M5)</td>
|
||||
<td><code>https://m5api.momentry.ddns.net</code></td>
|
||||
<td>Remote access</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Variables</h2>
|
||||
<p>All examples in this documentation use these environment variables:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">API</span><span class="o">=</span><span class="s2">"http://localhost:3003"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"your-api-key-here"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Authentication</h2>
|
||||
<p>All endpoints under <code>/api/v1/*</code> require authentication.
|
||||
The following endpoints are public (no auth needed):</p>
|
||||
<ul>
|
||||
<li><code>GET /health</code></li>
|
||||
<li><code>POST /api/v1/auth/login</code></li>
|
||||
<li><code>POST /api/v1/auth/logout</code></li>
|
||||
</ul>
|
||||
<h3>Three Authentication Modes</h3>
|
||||
<p>The system supports three authentication methods, checked in <strong>priority order</strong> by the middleware:</p>
|
||||
<div class="codehilite"><pre><span></span><code>Middleware priority:
|
||||
1. Session Cookie (Portal/browser)
|
||||
2. JWT Bearer (API clients: n8n, CLI)
|
||||
3. API Key Header (legacy compatibility)
|
||||
4. API Key Query Param (?api_key=)
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mode</th>
|
||||
<th>Transport</th>
|
||||
<th>Expiry</th>
|
||||
<th>Scope</th>
|
||||
<th>Best for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td><code>Cookie: session_id=<uuid></code></td>
|
||||
<td>24h</td>
|
||||
<td>per-browser session</td>
|
||||
<td>Portal (browser)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td><code>Authorization: Bearer <token></code></td>
|
||||
<td>1h</td>
|
||||
<td>per-login token</td>
|
||||
<td>API clients (n8n, CLI, scripts)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td><code>X-API-Key: <key></code></td>
|
||||
<td>90d</td>
|
||||
<td>fixed key for automation</td>
|
||||
<td>Legacy scripts, WordPress</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Login</h3>
|
||||
<p><strong>Default accounts & API keys:</strong></p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>API Key</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>admin</code></td>
|
||||
<td><code>admin</code></td>
|
||||
<td>—</td>
|
||||
<td>admin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>muser_demo_key_32chars_abcdef1234567890</code></td>
|
||||
<td>user</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The demo API key is set via <code>MOMENTRY_DEMO_API_KEY</code> env var and can be used in place of JWT for marcom integrations:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Using API key instead of JWT</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: muser_demo_key_32chars_abcdef1234567890"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login as admin</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "admin", "password": "admin"}'</span>
|
||||
|
||||
<span class="c1"># Login as demo user</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "demo", "password": "demo"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Success Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"jwt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJIUzI1NiIs..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"muser_..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"expires_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-18T13:00:00Z"</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>jwt</code></td>
|
||||
<td>string</td>
|
||||
<td>JWT access token. Use as <code>Authorization: Bearer <jwt></code>. Expires in 1 hour.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>api_key</code></td>
|
||||
<td>string</td>
|
||||
<td>Legacy API key. Use as <code>X-API-Key: <key></code>. Good for 90 days.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.username</code></td>
|
||||
<td>string</td>
|
||||
<td>Username</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.role</code></td>
|
||||
<td>string</td>
|
||||
<td>Role: <code>admin</code>, <code>user</code>, or <code>readonly</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>expires_at</code></td>
|
||||
<td>string</td>
|
||||
<td>ISO8601 timestamp of JWT expiration</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The login endpoint also sets a <code>Set-Cookie</code> header for browser-based clients:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nt">Set-Cookie</span><span class="o">:</span><span class="w"> </span><span class="nt">session_id</span><span class="o">=<</span><span class="nt">uuid</span><span class="o">>;</span><span class="w"> </span><span class="nt">Path</span><span class="o">=/</span><span class="nt">api</span><span class="o">;</span><span class="w"> </span><span class="nt">HttpOnly</span><span class="o">;</span><span class="w"> </span><span class="nt">SameSite</span><span class="o">=</span><span class="nt">Strict</span><span class="o">;</span><span class="w"> </span><span class="nt">Max-Age</span><span class="o">=</span><span class="nt">86400</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Response (401)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid username or password"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Using JWT</h3>
|
||||
<p>JWT is preferred for API clients (n8n, CLI scripts, WordPress). It is validated by the middleware without a database lookup (stateless).</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login and capture JWT</span>
|
||||
<span class="nv">JWT</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">"import json,sys;print(json.load(sys.stdin)['jwt'])"</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># Use JWT for all subsequent requests</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>JWT is short-lived (1 hour). When it expires, request a new one via login.</p>
|
||||
<hr />
|
||||
<h3>Using Session Cookie (Browser)</h3>
|
||||
<p>Browser-based clients (Portal) get a session cookie automatically after login. The browser sends the cookie with every request—no manual header needed.</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login captures the session cookie from Set-Cookie header</span>
|
||||
curl<span class="w"> </span>-v<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span>
|
||||
|
||||
<span class="c1"># Browser automatically sends: Cookie: session_id=<uuid></span>
|
||||
<span class="c1"># No manual header needed for subsequent requests</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>The session cookie is HttpOnly (not accessible from JavaScript) and SameSite=Strict (protected against CSRF).</p>
|
||||
<hr />
|
||||
<h3>Using Legacy API Key</h3>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
|
||||
<span class="c1"># Also accepted via Bearer header (non-JWT format) or query parameter:</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?api_key=</span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>API keys are validated via SHA256 hash lookup in the database. They are long-lived (90 days) and intended for automation.</p>
|
||||
<h3>Obtaining an API Key (CLI)</h3>
|
||||
<div class="codehilite"><pre><span></span><code>momentry<span class="w"> </span>api-key<span class="w"> </span>create<span class="w"> </span><span class="s2">"My API Key"</span><span class="w"> </span>--key-type<span class="w"> </span>user
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Logout</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Logout using the session cookie (browser)</span>
|
||||
curl<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=<uuid>"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>What logout does</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Auth mode</th>
|
||||
<th>Effect</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td>Session deleted from database. Same cookie returns 401 on subsequent requests.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td>JWT remains valid until expiry. (JWT is stateless — logout adds JWT to a blacklist only if API key mode is used.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td>API key remains valid. (Legacy keys are shared across sessions — revoking would break other clients.)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example: full session lifecycle</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># 1. Login</span>
|
||||
<span class="nv">SESSION_ID</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-D<span class="w"> </span>-<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">'s/.*session_id=\([^;]*\).*/\1/'</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># 2. Use session (works)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 200</span>
|
||||
|
||||
<span class="c1"># 3. Logout</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → {"success": true}</span>
|
||||
|
||||
<span class="c1"># 4. Use session again (rejected)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 401</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Authentication Flow Summary</h3>
|
||||
<div class="codehilite"><pre><span></span><code>Login Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 1. Check users │ ← users table (argon2 password verify)
|
||||
│ table │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
┌───┴───┐
|
||||
│ match │
|
||||
└───┬───┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 2. Create JWT │ ← 1h expiry, signed with JWT_SECRET
|
||||
├──────────────────┤
|
||||
│ 3. Create │ ← 24h expiry, stored in sessions table
|
||||
│ session │
|
||||
├──────────────────┤
|
||||
│ 4. Set-Cookie │ ← HttpOnly, SameSite=Strict, Path=/api
|
||||
├──────────────────┤
|
||||
│ 5. Return │ ← JWT + api_key + user info to client
|
||||
└──────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code>Protected Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Middleware checks: │
|
||||
│ │
|
||||
│ 1. Cookie session? │ → DB lookup session → get api_key → verify
|
||||
│ │
|
||||
│ 2. JWT Bearer? │ → verify JWT signature → decode claims
|
||||
│ │
|
||||
│ 3. X-API-Key? │ → SHA256 hash → DB lookup → verify
|
||||
│ │
|
||||
│ 4. ?api_key=? │ → same as #3
|
||||
│ │
|
||||
│ 5. None → 401 │
|
||||
└──────────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Error Responses</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Session expired or logged out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>JWT expired</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>API key revoked or inactive</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Related</h3>
|
||||
<ul>
|
||||
<li><code>POST /api/v1/resource/tmdb/check</code> — test authentication + TMDb API connectivity</li>
|
||||
<li><code>GET /health/detailed</code> — view auth status (integrations section)</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>File Registration</h2>
|
||||
<h3><code>POST /api/v1/files/register</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Register a video file for processing. Returns the file's metadata and UUID.</p>
|
||||
<p><strong>New in v0.1.2</strong>: Registration now <strong>automatically triggers the processing pipeline</strong> — no need to call <code>POST /api/v1/file/:uuid/process</code> separately. The system will:
|
||||
1. Register the file and run ffprobe
|
||||
2. Auto-run offline TMDb probe (reads local identity files, no API calls)
|
||||
3. Create a monitor job for the worker
|
||||
4. Worker starts all 10 processors (Cut → ASR → ASRX → YOLO → OCR → Face → Pose → VisualChunk → Story → 5W1H)</p>
|
||||
<p>If the file already exists (same content hash), returns the existing record with <code>already_exists: true</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Path to video file on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex pattern for batch register (requires <code>file_path</code> to be a directory)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>User ID to associate with registration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>content_hash</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Pre-computed SHA-256 hash (skips computation)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Register a single file</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/video.mp4"}'</span>
|
||||
|
||||
<span class="c1"># Batch register files matching a pattern in a directory</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'</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">"success"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">120.5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">2892</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"File registered successfully"</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 of the registered file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name (auto-renamed if name conflict)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Canonical path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_type</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"video"</code>, <code>"audio"</code>, or <code>"unknown"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>duration</code></td>
|
||||
<td>float</td>
|
||||
<td>Duration in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video width in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video height in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>Frames per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_frames</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total frame count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>already_exists</code></td>
|
||||
<td>boolean</td>
|
||||
<td>True if same content was already registered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>string</td>
|
||||
<td>Human-readable status</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>401</code></td>
|
||||
<td>Missing or invalid API key</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>Invalid request body</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File path does not exist</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/files/scan</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Scan the filesystem directory and list all media files, showing which are registered, processing, or unregistered.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number (1-based)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Items per page (alias: <code>limit</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Max items (alias for <code>page_size</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex filter on file name (e.g., <code>.*\\.mp4$</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_by</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>name</code></td>
|
||||
<td>Sort field: <code>name</code>, <code>size</code>, <code>modified</code>, <code>status</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_order</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>asc</code></td>
|
||||
<td>Sort direction: <code>asc</code> or <code>desc</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Full scan</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, registered_count, unregistered_count}'</span>
|
||||
|
||||
<span class="c1"># Paginated (page 1, 5 per page)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?page=1&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{page, total_pages, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Regex filter: only mp4 files</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?pattern=.*\\.mp4</span>$<span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{filtered_total, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Sort by file size (largest first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=size&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, file_size}]'</span>
|
||||
|
||||
<span class="c1"># Sort by modified time (most recent first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=modified&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, modified_time}]'</span>
|
||||
|
||||
<span class="c1"># Sort by status</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=status&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, status}]'</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">"files"</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">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">12345678</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_registered"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"registration_time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">107</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"filtered_total"</span><span class="p">:</span><span class="w"> </span><span class="mi">80</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page"</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">"page_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_pages"</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">"registered_count"</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">"unregistered_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">81</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>files</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of file info objects (paginated)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].relative_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Path relative to scan root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Absolute path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>File size in bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].modified_time</code></td>
|
||||
<td>string</td>
|
||||
<td>Last modified timestamp (ISO8601)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].is_registered</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Whether file is registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"completed"</code>, <code>"processing"</code>, <code>"registered"</code>, <code>"unregistered"</code>, or <code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].registration_time</code></td>
|
||||
<td>string</td>
|
||||
<td>DB registration timestamp (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].job_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Processing job ID (only if a job exists)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total files found on disk (unfiltered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>filtered_total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files matching regex filter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>Current page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_pages</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total pages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>registered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>unregistered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files not yet registered</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Notes</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Behavior</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Regex</strong></td>
|
||||
<td>Case-insensitive (<code>(?i)</code> prefix auto-applied). Applied to <code>file_name</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sort order</strong></td>
|
||||
<td>Default (<code>sort_by=name</code>): registered files first, then alphabetically. <code>sort_by=status</code>: alphabetical by status string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Pagination</strong></td>
|
||||
<td><code>page_size</code> and <code>limit</code> are aliases. Default: show all results.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Processing order</strong></td>
|
||||
<td><code>pattern</code> regex filter → <code>sort_by</code>/<code>sort_order</code> → <code>page</code>/<code>page_size</code> slice.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>TMDb Enrichment</h2>
|
||||
<blockquote>
|
||||
<p>⚠️ <strong>External resource</strong>: TMDb requires internet access, violating Momentry's local-only principle.
|
||||
All core processing (ASR, YOLO, Face, OCR, Pose, embeddings) runs fully offline.
|
||||
TMDb enrichment is <strong>optional</strong> and gated behind <code>TMDB_API_KEY</code> + <code>MOMENTRY_TMDB_PROBE_ENABLED</code>.</p>
|
||||
</blockquote>
|
||||
<h3>Overview</h3>
|
||||
<p>TMDb enrichment is an optional identity enrichment step that can be run after Pipeline face detection completes. The workflow is:</p>
|
||||
<ol>
|
||||
<li><strong>Prefetch</strong> (requires internet): Download movie cast data from TMDb API → cache to <code>{file_uuid}.tmdb.json</code></li>
|
||||
<li><strong>Probe</strong>: Read local cache → create identities for <strong>all</strong> cast members (<code>source='tmdb'</code>) + save <code>identity.json</code> + download profile image to <code>{OUTPUT}/identities/{uuid}/profile.jpg</code></li>
|
||||
<li><strong>Match</strong>: The worker automatically matches video faces against TMDb identities when <code>MOMENTRY_TMDB_PROBE_ENABLED=true</code></li>
|
||||
</ol>
|
||||
<h3><code>POST /api/v1/agents/tmdb/prefetch</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Fetch TMDb cast data for a registered file and cache it locally. This is the only step requiring internet access.</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 UUID to enrich</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/agents/tmdb/prefetch"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"cache_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/output/...tmdb.json"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/file/:file_uuid/tmdb-probe</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Read local TMDb cache and create/update identities. Requires prefetch to have been run first.</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tmdb-probe"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_created, movie_title}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — identities created)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</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">"identities_created"</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">"movie_title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — no cache)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"No TMDb cache found. Run tmdb-prefetch first."</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>GET /api/v1/resource/tmdb</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>View TMDb resource status including configuration, identity counts, and cache file count.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_seeded, cache_files}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/resource/tmdb/check</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Ping the TMDb API to verify connectivity and measure latency.</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb/check"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.status'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"api_key_configured"</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">"enabled"</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">"api_reachable"</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">"api_latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">120</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
docs_v1.0/doc_user/index.html
Normal file
26
docs_v1.0/doc_user/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>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: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
p.subtitle { color: #666; margin-bottom: 24px; }
|
||||
ul { list-style: none; }
|
||||
li { padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
li:last-child { border: none; }
|
||||
a { color: #0066cc; text-decoration: none; font-size: 16px; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Momentry API Documentation</h1>
|
||||
<p class="subtitle">Generated from API_WORKSPACE modules</p>
|
||||
<ul><li><a href="API_ACCESS.html">Api Access</a></li><li><a href="API_ENDPOINTS.html">Api Endpoints</a></li><li><a href="API_ERROR_CODES.html">Api Error Codes</a></li><li><a href="API_INDEX.html">Api Index</a></li><li><a href="API_QUICK_REFERENCE.html">Api Quick Reference</a></li><li><a href="API_REFERENCE.html">Api Reference</a></li><li><a href="API_TRAINING_MARCOM.html">Api Training Marcom</a></li><li><a href="Demo_EndToEnd.html">Demo Endtoend</a></li><li><a href="M5API_Pipeline_Demo.html">M5Api Pipeline Demo</a></li><li><a href="TMDb_User_Guide.html">Tmdb User Guide</a></li></ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
46
docs_v1.0/doc_user/login.html
Normal file
46
docs_v1.0/doc_user/login.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -66,6 +66,7 @@ const MODULES = [
|
||||
["10_pipeline","生產線","Pipeline"],
|
||||
["12_agent","智慧代理","AI Agents"],
|
||||
["13_config","系統設定","System Config"],
|
||||
["14_identity_history","操作歷史","Operation History (Undo/Redo)"],
|
||||
];
|
||||
|
||||
const el = document.getElementById('content');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
### `POST /api/v1/search/smart`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector `story_parent` and `llm_parent` chunks by cosine similarity.
|
||||
|
||||
@@ -15,13 +15,22 @@ Semantic vector search using EmbeddingGemma-300m. Generates a query embedding vi
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | Yes | — | File UUID to search within |
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | File UUID to search within. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 5 | Max results to return |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 5 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Audrey Hepburn"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
@@ -37,6 +46,7 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
"query": "Audrey Hepburn",
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"parent_id": 1087822,
|
||||
"scene_order": 1087822,
|
||||
"start_frame": 104438,
|
||||
@@ -54,12 +64,16 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where result was found |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/universal`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`.
|
||||
|
||||
@@ -68,13 +82,22 @@ Multi-type BM25 full-text search across chunks, frames, and persons. Uses Postgr
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `types` | string[] | No | `["chunk","frame","person"]` | Search types |
|
||||
| `limit` | integer | No | 10 | Max results per type |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Cary Grant"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
@@ -90,6 +113,7 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"results": [
|
||||
{
|
||||
"type": "chunk",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "story_child",
|
||||
"start_frame": 5103,
|
||||
@@ -98,6 +122,25 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"end_time": 213.64,
|
||||
"text": "[213s-214s] Cary Grant: \"Olá!\"",
|
||||
"score": 0.9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"frame_number": 5105,
|
||||
"timestamp": 212.72,
|
||||
"score": 0.7,
|
||||
"objects": null,
|
||||
"ocr_texts": null,
|
||||
"faces": null
|
||||
},
|
||||
{
|
||||
"type": "person",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"identity_id": 12,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"appearance_count": 542,
|
||||
"score": 0.95
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
@@ -105,23 +148,78 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].type` | string | Result type: `chunk`, `frame`, or `person` |
|
||||
| `results[].file_uuid` | string | File UUID where result was found (all types) |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/frames`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search face detection frames by identity name or trace ID.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/identity_text`
|
||||
### `GET /api/v1/search/identity_text`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search text chunks spoken by a specific identity.
|
||||
Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 50 | Items per page |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?file_uuid=$FILE_UUID&q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "llm_parent_..._256_270",
|
||||
"start_time": 256.256,
|
||||
"end_time": 270.228,
|
||||
"text_content": "...lack of affection...",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 94
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where chunk was found |
|
||||
| `results[].identity_id` | integer | Identity ID if face was detected |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,4 +243,4 @@ Search text chunks spoken by a specific identity.
|
||||
| **Storage** | pgvector (`chunk.embedding` column) |
|
||||
|
||||
---
|
||||
*Updated: 2026-05-19 12:49:24*
|
||||
*Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs*
|
||||
|
||||
@@ -70,7 +70,16 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID" -H "X-API-Key: $KEY"
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Delete an identity permanently.
|
||||
Delete an identity permanently. All face detections bound to this identity are unbound (`identity_id` set to `NULL`). The identity JSON file is deleted from disk.
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every DELETE records a full snapshot of the identity and its unbound faces. See [`14_identity_history.md`](14_identity_history.md#4-delete-history--undoredo) for:
|
||||
|
||||
- Undo via `POST /api/v1/identity/:identity_uuid/undo` — recreates identity and re-binds faces
|
||||
- Redo via `POST /api/v1/identity/:identity_uuid/redo` — re-deletes the identity
|
||||
|
||||
**Note**: Delete undo/redo reuses the same endpoints as PATCH undo/redo. The endpoint automatically detects whether the identity was deleted (undo) or needs to be re-deleted (redo) based on the history record.
|
||||
|
||||
---
|
||||
|
||||
@@ -129,124 +138,75 @@ curl -s -X PATCH "$API/api/v1/identity/$IDENTITY_UUID" \
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No fields to update or invalid UUID format |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every bind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert a bind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone bind
|
||||
- `GET /api/v1/identity/:identity_uuid/bind/history` — Query bind operations
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/files`
|
||||
## Metadata (Embedded JSON)
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
The `identities.metadata` column is a **JSONB** field that stores arbitrary structured data alongside the identity's core fields (name, status, identity_type). No schema is enforced — any valid JSON object is accepted.
|
||||
|
||||
Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range.
|
||||
### Merge Behavior
|
||||
|
||||
#### Example
|
||||
| Operation | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| **PATCH** | Shallow top-level merge: `COALESCE(metadata,'{}'::jsonb) \|\| $1::jsonb` | Sending `{"tmdb_rating": 8.5}` only adds/overwrites `tmdb_rating`; all other existing keys are preserved. |
|
||||
| **mergeinto** | Recursive deep merge — nested sub-keys are merged individually, not replaced wholesale | Target has `{"tmdb": {"biography": "..."}}`, source has `{"tmdb": {"birthday": "1904-01-18"}}` → result is `{"tmdb": {"biography": "...", "birthday": "1904-01-18"}}`. |
|
||||
| **Upload (`POST`)** | Direct overwrite — the entire `metadata` field is replaced with the request value. | |
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/files" -H "X-API-Key: $KEY"
|
||||
```
|
||||
### Validation
|
||||
|
||||
---
|
||||
| Scenario | Result |
|
||||
|----------|--------|
|
||||
| PATCH with non-object metadata (`string`, `array`, `number`, `null`) | `400 Bad Request: "metadata must be a JSON object"` |
|
||||
| mergeinto with non-object metadata | Accepted (mergeinto validates at application level) |
|
||||
| Upload with non-object metadata | Accepted (upload replaces directly) |
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/faces`
|
||||
### Conventional Keys
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
| Key | Type | Writer | Purpose |
|
||||
|-----|------|--------|---------|
|
||||
| `aliases` | `[{locale, name}]` | PATCH, mergeinto | Multilingual display names (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| `merged_into` | `{uuid, at}` | mergeinto | Marks an identity as merged (undo mechanism reads this) |
|
||||
| `tmdb_*` | various | TMDb probe | Movie metadata (biography, birthday, known_for, etc.). Written only when `MOMENTRY_TMDB_PROBE_ENABLED=true`. |
|
||||
| `source` | string | mergeinto | Tagged on aliases/metadata when added by merge (`"merge"` value) |
|
||||
|
||||
Get all face detection records associated with this identity.
|
||||
Custom keys are fully supported — no registration required.
|
||||
|
||||
#### Example
|
||||
### Search Coverage
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/faces" -H "X-API-Key: $KEY"
|
||||
```
|
||||
The identity search endpoint (`GET /api/v1/identity/search`) matches across three scopes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File where face was detected |
|
||||
| `frame_number` | integer | Frame number of detection |
|
||||
| `face_id` | string | Face ID (format: `face_{frame_number}`) |
|
||||
| `confidence` | float | Detection confidence |
|
||||
1. `i.name` — exact and ILIKE against display name
|
||||
2. `jsonb_array_elements(i.metadata->'aliases')->>'name'` — locale-tagged alias names
|
||||
3. `i.metadata::text ILIKE $1` — raw string search across the entire JSON blob (all keys, all values)
|
||||
|
||||
---
|
||||
This means searching for `"1904-01-18"` or `"biography"` will match identities whose metadata contains those strings anywhere.
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/chunks`
|
||||
### History Snapshots
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
Every `identity_history` record captures the **full metadata** in both `before_snapshot` and `after_snapshot` (as part of the complete identity JSONB dump). Undo restores the identity row — including metadata — to the `before_snapshot` state.
|
||||
|
||||
Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said.
|
||||
For merge operations, the MongoDB merge history records `metadata_fields_added` and `metadata_fields_added_paths` (dot-separated paths like `"tmdb.biography"`). Merge undo removes only those specific paths, preserving subsequent manual edits to other metadata keys.
|
||||
|
||||
#### Example
|
||||
### Best Practices
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/chunks" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "bd80fec92b0b6963d177a2c55bf713e2",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "sentence",
|
||||
"start_frame": 5103,
|
||||
"end_frame": 5127,
|
||||
"fps": 24.0,
|
||||
"start_time": 212.64,
|
||||
"end_time": 213.64,
|
||||
"text_content": "[213s-214s] Cary Grant: \"Olá!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File identifier |
|
||||
| `chunk_id` | string | Sentence chunk identifier |
|
||||
| `start_frame` | integer | Frame-accurate start position |
|
||||
| `end_frame` | integer | Frame-accurate end position |
|
||||
| `fps` | float | Frames per second |
|
||||
| `start_time` | float | Start time in seconds |
|
||||
| `end_time` | float | End time in seconds |
|
||||
| `text_content` | string | Spoken text content |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/bind`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Bind a face detection to an identity. Associates the face trace with the identity for future search and recognition.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File where face is detected |
|
||||
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
|
||||
|
||||
#### Side Effects
|
||||
|
||||
- 清除該 face detection row 的 `stranger_id`(設為 NULL)
|
||||
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "face_id": "1_5"}'
|
||||
```
|
||||
| Guideline | Reason |
|
||||
|-----------|--------|
|
||||
| Deep nesting is allowed in metadata | All metadata merge operations use `jsonb_deep_merge()` — nested sub-keys are merged recursively, not replaced wholesale |
|
||||
| Use `aliases` for display names | Frontend has built-in locale fallback logic (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| Avoid >1MB per identity | Metadata is included in search indexing (`metadata::text ILIKE`); large blobs degrade query performance |
|
||||
| Don't rely on metadata ordering | JSONB preserves insertion order but PostgreSQL does not guarantee it across operations |
|
||||
| No LLM/Gemma4 agent writes to metadata | Only API endpoints (PATCH, mergeinto, upload) and TMDb probe modify `identities.metadata` |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +255,10 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Trace bind operations share the same history/undo/redo system as single-face binds. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for endpoints.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/traces`
|
||||
@@ -382,6 +346,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
- 被 unbind 的 face 不會自動成為 stranger
|
||||
- 要重新標記為 stranger 需重新跑 Agent API(`identity/analyze`)
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Unbind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert an unbind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone unbind
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/mergeinto`
|
||||
@@ -391,6 +362,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
|
||||
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
|
||||
|
||||
#### Two Merge Cases
|
||||
|
||||
| Case | Description | Undo/Redo Support |
|
||||
|------|-------------|-------------------|
|
||||
| **stranger → identity** | Merge an auto-generated stranger identity into a known identity (TMDb or user-defined) | ✅ 24hr undo/redo |
|
||||
| **identity A → identity B** | Merge two known identities (e.g., duplicate entries) | ✅ 24hr undo/redo |
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
@@ -402,8 +380,12 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
|
||||
- 轉移所有 `face_detections.identity_id` 到目標 identity
|
||||
- 同時清除所有被轉移 rows 的 `stranger_id`
|
||||
- 將 source name 加入 target aliases (with `source: "merge"` tag)
|
||||
- 將 source aliases 加入 target aliases (if not already present)
|
||||
- 將 source metadata fields 加入 target metadata (if not already present)
|
||||
- `keep_history: true`(預設):source identity 設為 `status='merged'`,保留記錄
|
||||
- `keep_history: false`:**刪除** source identity 及其 identity JSON 檔案
|
||||
- **記錄 merge history 到 MongoDB**(支援 undo/redo)
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -411,7 +393,7 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": true}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
@@ -419,11 +401,23 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
|
||||
"data": { "faces_transferred": 52 }
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, history kept)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_added": 1,
|
||||
"metadata_fields_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | Unique merge operation ID (for undo) |
|
||||
| `faces_transferred` | integer | Number of face detections transferred |
|
||||
| `aliases_added` | integer | Number of aliases added to target |
|
||||
| `metadata_fields_added` | integer | Number of metadata fields added to target |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
@@ -433,25 +427,189 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Search identities by name (ILIKE search). Returns matching identity records.
|
||||
Undo a merge operation within 24 hours. Restores the source identity and reverts face bindings.
|
||||
|
||||
#### Undo Behavior
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Restore source identity | If `keep_history=true`: restore status to `confirmed`<br>If `keep_history=false`: recreate identity from MongoDB snapshot |
|
||||
| Restore faces | Transfer faces back to source identity |
|
||||
| Remove aliases from target | Remove aliases with `source: "merge"` tag |
|
||||
| Remove metadata fields from target | Remove fields that were added from source |
|
||||
| **Preserve manual changes** | Keep aliases/metadata manually added after merge |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Cary" -H "X-API-Key: $KEY"
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/undo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Undo merge completed: 'stranger_13894' restored, 52 faces reverted",
|
||||
"data": {
|
||||
"source_identity_restored": {
|
||||
"uuid": "a9a90105...",
|
||||
"name": "stranger_13894",
|
||||
"status": "confirmed"
|
||||
},
|
||||
"faces_reverted": 52,
|
||||
"aliases_removed_from_target": 1,
|
||||
"metadata_fields_removed_from_target": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Undo deadline expired (>24hr) or already undone |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation. See [`14_identity_history.md`](14_identity_history.md#post-apiv1identitymergemerge_idredo) for full details.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/merge/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query merge history records from MongoDB.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `source_uuid` | string | No | — | Filter by source identity UUID |
|
||||
| `target_uuid` | string | No | — | Filter by target identity UUID |
|
||||
| `merge_id` | string | No | — | Filter by specific merge ID |
|
||||
| `undone` | bool | No | — | Filter by undone status |
|
||||
| `page` | int | No | 1 | Page number |
|
||||
| `page_size` | int | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/merge/history?page=1&page_size=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"results": [
|
||||
{
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_name": "stranger_13894",
|
||||
"target_name": "Louis Viret",
|
||||
"faces_transferred": 52,
|
||||
"merged_at": "2026-05-27T10:00:00Z",
|
||||
"undo_deadline": "2026-05-28T10:00:00Z",
|
||||
"undone": false,
|
||||
"undo_expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Identity name |
|
||||
| `source` | string | Identity source |
|
||||
| `tmdb_id` | integer | TMDb ID (if source = tmdb) |
|
||||
| `file_uuid` | string | Associated file |
|
||||
| `merge_id` | string | Unique merge operation ID |
|
||||
| `source_name` | string | Source identity name |
|
||||
| `target_name` | string | Target identity name |
|
||||
| `faces_transferred` | integer | Number of faces transferred |
|
||||
| `merged_at` | datetime | When merge occurred |
|
||||
| `undo_deadline` | datetime | 24hr deadline for undo |
|
||||
| `undone` | bool | Whether merge was undone |
|
||||
| `undo_expired` | bool | Whether undo deadline passed |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search identity name → find associated chunks. Searches identity name and aliases, returns identities with their associated text chunks.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match on name and aliases) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey&file_uuid=$FILE_UUID" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"identity_id": 9,
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "tmdb",
|
||||
"tmdb_id": 1932,
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"trace_id": 41,
|
||||
"chunk_id": "llm_parent_..._204_207",
|
||||
"start_time": 204.162,
|
||||
"text_content": "...confrontation..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].identity_id` | integer | Identity ID |
|
||||
| `results[].name` | string | Identity name |
|
||||
| `results[].source` | string | Identity source (`tmdb`, `user_defined`, etc.) |
|
||||
| `results[].tmdb_id` | integer | TMDb person ID (if source = tmdb) |
|
||||
| `results[].file_uuid` | string | File where identity appears |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
| `results[].chunk_id` | string | Associated chunk ID |
|
||||
| `results[].start_time` | float | Chunk start time |
|
||||
| `results[].text_content` | string | Chunk text content |
|
||||
|
||||
---
|
||||
|
||||
@@ -628,4 +786,4 @@ PATCH /api/v1/identity/:identity_uuid
|
||||
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
|
||||
|
||||
---
|
||||
*Updated: 2026-05-25
|
||||
*Updated: 2026-05-25 — Added `GET /api/v1/file/:file_uuid/faces` with 4 binding states, filters, strangers table split
|
||||
|
||||
696
docs_v1.0/doc_wasm/modules/14_identity_history.md
Normal file
696
docs_v1.0/doc_wasm/modules/14_identity_history.md
Normal file
@@ -0,0 +1,696 @@
|
||||
<!-- module: identity_history -->
|
||||
<!-- description: Identity operation history, undo, and redo (PATCH, bind, unbind, bind_trace, mergeinto) -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
## Identity Operation History
|
||||
|
||||
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.
|
||||
|
||||
Three independent undo/redo systems exist:
|
||||
|
||||
| System | Storage | Operations Covered |
|
||||
|--------|---------|-------------------|
|
||||
| **PATCH** | PostgreSQL `identity_history` | `update` |
|
||||
| **Bind** | PostgreSQL `identity_history` | `bind`, `unbind`, `bind_trace` |
|
||||
| **Merge** | MongoDB `identity_merge_history` | mergeinto |
|
||||
| **Delete** | PostgreSQL `identity_history` | `delete` |
|
||||
|
||||
---
|
||||
|
||||
### 1. PATCH History & Undo/Redo
|
||||
|
||||
#### Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | Full identity record (all fields) before and after each PATCH |
|
||||
| Max records | 256 per identity (oldest auto-deleted when limit exceeded) |
|
||||
| Undo steps | Unlimited (no expiry, no step limit) |
|
||||
| Redo stack | Cleared on new PATCH (`is_undone=true` + `operation='update'` records are deleted) |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
PATCH 1 → PATCH 2 (undo stack)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
|
||||
```
|
||||
|
||||
A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent PATCH operations. Restores the identity's `before_snapshot` and marks the history records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply (max records undone in one call) |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores `name`, `identity_type`, `source`, `status`, `metadata`, `tmdb_id`, `tmdb_profile` from the last record's `before_snapshot`
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"undone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `current_state` | object | Full identity state after undo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone PATCH operations. Restores the identity's `after_snapshot` and marks the history records as no longer undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores all identity fields from the last record's `after_snapshot`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"redone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "John Smith",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": { "aliases": [...] },
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `current_state` | object | Full identity state after redo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to `operation='update'`).
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 5,
|
||||
"undo_stack_count": 3,
|
||||
"redo_stack_count": 2,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 42,
|
||||
"operation": "update",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 41,
|
||||
"operation": "update",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T11:30:00Z",
|
||||
"undone_at": "2026-05-27T13:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total PATCH history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`"update"` for PATCH) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the PATCH was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 2. Bind/Unbind/Trace History & Undo/Redo
|
||||
|
||||
All three operations (`bind`, `unbind`, `bind_trace`) share a single history table and undo/redo stack.
|
||||
|
||||
#### Bind Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table (same table as PATCH) |
|
||||
| Snapshot | `{"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}` |
|
||||
| Max records | 256 per identity (shared limit across all operation types) |
|
||||
| Undo steps | Unlimited (`steps` param) |
|
||||
| Redo stack | Cleared on new bind/unbind/bind_trace (`operation IN ('bind','unbind','bind_trace')` + `is_undone=true` records deleted) |
|
||||
| Stack isolation | Bind redo stack is **independent** from PATCH redo stack — clearing one does not affect the other |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
A new bind/unbind/trace after undo clears only the bind redo stack (operations with `IN ('bind','unbind','bind_trace')`).
|
||||
|
||||
##### Snapshot Format
|
||||
|
||||
**Before (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": null
|
||||
}
|
||||
```
|
||||
|
||||
**After (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": 9
|
||||
}
|
||||
```
|
||||
|
||||
**Before (unbind) — binding existed before:**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": 9
|
||||
}
|
||||
```
|
||||
|
||||
**After (unbind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": null
|
||||
}
|
||||
```
|
||||
|
||||
For `bind_trace`, the snapshot uses `trace_id` instead of `face_id`, with `identity_id_before` capturing the first face's identity in that trace.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent bind/unbind/bind_trace operations. Restores `identity_id_before` from the snapshot and marks records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_before` — for bind this is `null` (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "bind",
|
||||
"undone_count": 1,
|
||||
"affected_rows": 53
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation undone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone bind/unbind/bind_trace operations. Restores `identity_id_after` from the snapshot.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_after` — for bind this is the identity the face was bound to, for unbind this is `null`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "unbind",
|
||||
"redone_count": 1,
|
||||
"affected_rows": 1
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation redone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/bind/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 3,
|
||||
"undo_stack_count": 2,
|
||||
"redo_stack_count": 1,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 52,
|
||||
"operation": "bind_trace",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T14:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 51,
|
||||
"operation": "unbind",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T13:00:00Z",
|
||||
"undone_at": "2026-05-27T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"history_id": 50,
|
||||
"operation": "bind",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total bind history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the operation was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/bind/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 3. Merge History & Undo/Redo
|
||||
|
||||
Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.
|
||||
|
||||
#### Merge Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | MongoDB `identity_merge_history` collection |
|
||||
| Snapshot | Full source identity state + target identity state + aliases/metadata diffs |
|
||||
| Trigger | Every mergeinto with `keep_history=true` |
|
||||
| Undo deadline | 24 hours (renewed on redo) |
|
||||
| Redo support | Yes — restores undone merges with new 24hr deadline |
|
||||
| Max records | Unlimited |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
Already documented in [`07_identity.md`](07_identity.md#post-apiv1identitymergemerge_idundo). See that document for full details.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation within the renewed 24-hour deadline.
|
||||
|
||||
##### Request
|
||||
|
||||
No body required. The merge ID is taken from the URL path.
|
||||
|
||||
##### Behavior
|
||||
|
||||
1. Validates the merge record exists and `undone=true` (not already active)
|
||||
2. Checks the 24-hour undo deadline (if expired, the redo is rejected)
|
||||
3. Restores face bindings: moves all faces from `target_identity` back to `source_identity`
|
||||
4. Re-adds aliases that were removed by the undo (aliases with `source: "merge"` tag)
|
||||
5. Re-adds metadata fields that were removed by the undo
|
||||
6. If `keep_history=true`: sets `source_identity.status = 'merged'` again
|
||||
7. If `keep_history=false`: recreates source identity from the `undone_snapshot` stored at undo time
|
||||
8. Syncs both identity JSON files to disk
|
||||
9. Sets `undone=false`, clears `undone_snapshot`, renews `undo_deadline = NOW() + 24h`
|
||||
10. Records `redone_by` user for audit
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Redo merge completed: merged 'stranger_13894' into 'Louis Viret' (52 faces transferred)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_re_added": 1,
|
||||
"metadata_fields_re_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | The merge operation ID |
|
||||
| `faces_transferred` | integer | Number of faces transferred from source to target |
|
||||
| `aliases_re_added` | integer | Number of aliases restored to target |
|
||||
| `metadata_fields_re_added` | integer | Number of metadata fields restored to target |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Merge not undone, deadline expired, or cannot redo |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 4. Delete History & Undo/Redo
|
||||
|
||||
#### Delete Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | `{"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}` |
|
||||
| Max records | 1 active delete record per identity (redo stack cleared on new delete) |
|
||||
| Undo support | Yes — recreates identity row, re-binds faces |
|
||||
| Redo support | Yes — re-deletes the identity |
|
||||
| Identity file | Deleted on delete, recreated on undo |
|
||||
|
||||
#### Snapshot Format
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"id": 9,
|
||||
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
},
|
||||
"unbound_faces": [
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"trace_id": null
|
||||
},
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_6",
|
||||
"trace_id": 906
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Stack Model
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
A new delete after an undo clears the delete redo stack (no redo possible for the old delete).
|
||||
|
||||
#### Undo Behavior (via existing `POST /api/v1/identity/:identity_uuid/undo`)
|
||||
|
||||
1. Normal identity lookup fails (row was deleted)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=false` matching the UUID in the snapshot
|
||||
3. Recreates the identity row (new internal `id`, same UUID)
|
||||
4. Re-binds all faces listed in `unbound_faces` to the new identity
|
||||
5. Deletes the `identity_history` delete record as `is_undone=true` with `undone_at=NOW()`
|
||||
6. Syncs `identity.json` to disk
|
||||
7. Updates `_index.json`
|
||||
|
||||
#### Redo Behavior (via existing `POST /api/v1/identity/:identity_uuid/redo`)
|
||||
|
||||
1. Identity lookup succeeds (identity was restored by prior undo)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=true` matching the identity_id
|
||||
3. Deletes `identity.json` from disk
|
||||
4. Unbinds all faces (`identity_id = NULL`)
|
||||
5. Deletes the identity row
|
||||
6. Marks the delete history record as `is_undone=false`
|
||||
7. Returns success
|
||||
|
||||
#### Error Responses (delete undo/redo)
|
||||
|
||||
| HTTP | Scenario |
|
||||
|------|----------|
|
||||
| `400` | No delete history available (either no delete or already undone/redone) |
|
||||
| `404` | Identity not found (for redo — identity wasn't restored) |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo
|
||||
|
||||
| Aspect | PATCH Undo/Redo | Bind Undo/Redo | Merge Undo/Redo | Delete Undo/Redo |
|
||||
|--------|----------------|----------------|-----------------|------------------|
|
||||
| Storage | PostgreSQL `identity_history` | PostgreSQL `identity_history` | MongoDB `identity_merge_history` | PostgreSQL `identity_history` |
|
||||
| Operation filter | `operation='update'` | `operation IN ('bind','unbind','bind_trace')` | — | `operation='delete'` |
|
||||
| Trigger | Every PATCH | Every bind/unbind/bind_trace | Every mergeinto with `keep_history=true` | Every DELETE |
|
||||
| Undo deadline | None (unlimited) | None (unlimited) | 24 hours (renewed on redo) | None (unlimited) |
|
||||
| Redo support | Yes | Yes | Yes | Yes |
|
||||
| Step undo | Yes (`steps` param) | Yes (`steps` param) | No (full undo/redo only) | No (single record) |
|
||||
| Max records | 256 per identity | 256 per identity (shared) | Unlimited | 256 per identity (shared) |
|
||||
| User tracking | `user_id` + `user_source` | `user_id` + `user_source` | `performed_by_user` + `undone_by` / `redone_by` | `user_id` + `user_source` |
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-28*
|
||||
Binary file not shown.
Reference in New Issue
Block a user