From 6452ac5af2943d4ff1a7607a9bb4e7ce4440164c Mon Sep 17 00:00:00 2001 From: Accusys Date: Mon, 18 May 2026 10:07:38 +0800 Subject: [PATCH] feat: WASM-based doc viewer (pulldown-cmark) --- doc_wasm/Cargo.lock | 224 ++++++++++++ doc_wasm/Cargo.toml | 18 + doc_wasm/src/lib.rs | 29 ++ docs_v1.0/API_WORKSPACE/Makefile | 15 + docs_v1.0/doc_wasm/index.html | 127 +++++++ docs_v1.0/doc_wasm/modules/01_auth.md | 280 +++++++++++++++ docs_v1.0/doc_wasm/modules/02_health.md | 147 ++++++++ docs_v1.0/doc_wasm/modules/03_register.md | 184 ++++++++++ docs_v1.0/doc_wasm/modules/04_lookup.md | 138 ++++++++ docs_v1.0/doc_wasm/modules/05_process.md | 236 +++++++++++++ docs_v1.0/doc_wasm/modules/06_search.md | 145 ++++++++ docs_v1.0/doc_wasm/modules/07_identity.md | 333 ++++++++++++++++++ .../doc_wasm/modules/08_identity_agent.md | 65 ++++ docs_v1.0/doc_wasm/modules/08_media.md | 146 ++++++++ docs_v1.0/doc_wasm/modules/09_tmdb.md | 109 ++++++ docs_v1.0/doc_wasm/modules/10_pipeline.md | 178 ++++++++++ docs_v1.0/doc_wasm/modules/11_error_codes.md | 57 +++ docs_v1.0/doc_wasm/modules/12_agent.md | 118 +++++++ docs_v1.0/doc_wasm/pkg/doc_wasm.js | 232 ++++++++++++ docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm | Bin 0 -> 228029 bytes src/api/server.rs | 54 ++- 21 files changed, 2828 insertions(+), 7 deletions(-) create mode 100644 doc_wasm/Cargo.lock create mode 100644 doc_wasm/Cargo.toml create mode 100644 doc_wasm/src/lib.rs create mode 100644 docs_v1.0/API_WORKSPACE/Makefile create mode 100644 docs_v1.0/doc_wasm/index.html create mode 100644 docs_v1.0/doc_wasm/modules/01_auth.md create mode 100644 docs_v1.0/doc_wasm/modules/02_health.md create mode 100644 docs_v1.0/doc_wasm/modules/03_register.md create mode 100644 docs_v1.0/doc_wasm/modules/04_lookup.md create mode 100644 docs_v1.0/doc_wasm/modules/05_process.md create mode 100644 docs_v1.0/doc_wasm/modules/06_search.md create mode 100644 docs_v1.0/doc_wasm/modules/07_identity.md create mode 100644 docs_v1.0/doc_wasm/modules/08_identity_agent.md create mode 100644 docs_v1.0/doc_wasm/modules/08_media.md create mode 100644 docs_v1.0/doc_wasm/modules/09_tmdb.md create mode 100644 docs_v1.0/doc_wasm/modules/10_pipeline.md create mode 100644 docs_v1.0/doc_wasm/modules/11_error_codes.md create mode 100644 docs_v1.0/doc_wasm/modules/12_agent.md create mode 100644 docs_v1.0/doc_wasm/pkg/doc_wasm.js create mode 100644 docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm diff --git a/doc_wasm/Cargo.lock b/doc_wasm/Cargo.lock new file mode 100644 index 0000000..17bfbbf --- /dev/null +++ b/doc_wasm/Cargo.lock @@ -0,0 +1,224 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "doc_wasm" +version = "0.1.0" +dependencies = [ + "pulldown-cmark", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/doc_wasm/Cargo.toml b/doc_wasm/Cargo.toml new file mode 100644 index 0000000..6d638e1 --- /dev/null +++ b/doc_wasm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "doc_wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +pulldown-cmark = "0.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[profile.release] +lto = true +opt-level = "s" +strip = true diff --git a/doc_wasm/src/lib.rs b/doc_wasm/src/lib.rs new file mode 100644 index 0000000..6fd532f --- /dev/null +++ b/doc_wasm/src/lib.rs @@ -0,0 +1,29 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn render_markdown(md: &str) -> String { + let parser = pulldown_cmark::Parser::new(md); + let mut html = String::new(); + pulldown_cmark::html::push_html(&mut html, parser); + // wrap tables + html = html.replace("", "
"); + html +} + +#[wasm_bindgen] +pub fn module_list() -> String { + serde_json::to_string(&[ + ("01_auth", "安全認證", "Authentication"), + ("02_health", "健康檢查", "Health"), + ("03_register", "檔案註冊", "File Registration"), + ("04_lookup", "檔案屬性查詢", "File Lookup"), + ("05_process", "處理流程", "Processing"), + ("06_search", "搜尋功能", "Search"), + ("07_identity", "身份識別", "Identity"), + ("08_identity_agent", "智能身份綁定", "Smart Identity Binding"), + ("08_media", "串流與截圖", "Streaming & Thumbnails"), + ("09_tmdb", "TMDb 整合", "TMDb Integration"), + ("10_pipeline", "生產線", "Pipeline"), + ("12_agent", "智慧代理", "AI Agents"), + ]).unwrap_or_default() +} diff --git a/docs_v1.0/API_WORKSPACE/Makefile b/docs_v1.0/API_WORKSPACE/Makefile new file mode 100644 index 0000000..1d434d5 --- /dev/null +++ b/docs_v1.0/API_WORKSPACE/Makefile @@ -0,0 +1,15 @@ +PYTHON := /opt/homebrew/bin/python3.11 +WASM_PKG := ../../doc_wasm/pkg + +deploy: + @echo "Building HTML docs from modules..." + $(PYTHON) ../../scripts/build_docs.py + @echo " ✅ Updated ../doc/ (user docs)" + @echo " ✅ Updated ../doc_developer/ (developer docs)" + @echo "Building WASM doc..." + cd ../../doc_wasm && wasm-pack build --target web --no-opt 2>&1 | tail -2 + cp $(WASM_PKG)/doc_wasm_bg.wasm ../doc_wasm/pkg/ + cp $(WASM_PKG)/doc_wasm.js ../doc_wasm/pkg/ + cp ../../docs_v1.0/API_WORKSPACE/modules/0*.md ../doc_wasm/modules/ + cp ../../docs_v1.0/API_WORKSPACE/modules/1*.md ../doc_wasm/modules/ + @echo " ✅ Updated ../doc_wasm/ (WASM docs)" diff --git a/docs_v1.0/doc_wasm/index.html b/docs_v1.0/doc_wasm/index.html new file mode 100644 index 0000000..7789a3d --- /dev/null +++ b/docs_v1.0/doc_wasm/index.html @@ -0,0 +1,127 @@ + + + + + +Momentry API Docs (WASM) + + + +
+ +
+

Loading...

+
+
+ + + + diff --git a/docs_v1.0/doc_wasm/modules/01_auth.md b/docs_v1.0/doc_wasm/modules/01_auth.md new file mode 100644 index 0000000..96b8979 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/01_auth.md @@ -0,0 +1,280 @@ + + + + +## Base URL + +| Environment | URL | Purpose | +|-------------|-----|---------| +| Production | `http://localhost:3002` | Production deployment | +| External (M5) | `https://m5api.momentry.ddns.net` | Remote access | + +## Variables + +All examples in this documentation use these environment variables: + +```bash +API="http://localhost:3002" +KEY="your-api-key-here" +``` + +## Authentication + +All endpoints under `/api/v1/*` require authentication. +The following endpoints are public (no auth needed): + +- `GET /health` +- `POST /api/v1/auth/login` +- `POST /api/v1/auth/logout` + +### Three Authentication Modes + +The system supports three authentication methods, checked in **priority order** by the middleware: + +``` +Middleware priority: + 1. Session Cookie (Portal/browser) + 2. JWT Bearer (API clients, CLI) + 3. API Key Header (legacy compatibility) + 4. API Key Query Param (?api_key=) +``` + +| Mode | Transport | Expiry | Scope | Best for | +|------|-----------|--------|-------|----------| +| **Session Cookie** | `Cookie: session_id=` | 24h | per-browser session | Portal (browser) | +| **JWT** | `Authorization: Bearer ` | 1h | per-login token | API clients, CLI, scripts | +| **API Key** | `X-API-Key: ` | 90d | fixed key for automation | Legacy scripts, WordPress | + +--- + +### Login + +**Default accounts & API keys:** + +| Username | Password | API Key | Role | +|----------|----------|---------|------| +| `admin` | `admin` | — | admin | +| `demo` | `demo` | `muser_demo_key_32chars_abcdef1234567890` | user | + +The demo API key is set via `MOMENTRY_DEMO_API_KEY` env var and can be used in place of JWT for marcom integrations: + +```bash +# Using API key instead of JWT +curl -s "$API/api/v1/files/scan" -H "X-API-Key: muser_demo_key_32chars_abcdef1234567890" +``` + +```bash +# Login as admin +curl -s -X POST "$API/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username": "admin", "password": "admin"}' + +# Login as demo user +curl -s -X POST "$API/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username": "demo", "password": "demo"}' +``` + +#### Success Response + +```json +{ + "success": true, + "jwt": "eyJhbGciOiJIUzI1NiIs...", + "api_key": "muser_...", + "user": { + "username": "admin", + "role": "admin" + }, + "expires_at": "2026-05-18T13:00:00Z" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `jwt` | string | JWT access token. Use as `Authorization: Bearer `. Expires in 1 hour. | +| `api_key` | string | Legacy API key. Use as `X-API-Key: `. Good for 90 days. | +| `user.username` | string | Username | +| `user.role` | string | Role: `admin`, `user`, or `readonly` | +| `expires_at` | string | ISO8601 timestamp of JWT expiration | + +The login endpoint also sets a `Set-Cookie` header for browser-based clients: + +``` +Set-Cookie: session_id=; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400 +``` + +#### Error Response (401) + +```json +{ + "success": false, + "message": "Invalid username or password" +} +``` + +--- + +### Using JWT + +JWT is preferred for API clients (CLI scripts, WordPress). It is validated by the middleware without a database lookup (stateless). + +```bash +# Login and capture JWT +JWT=$(curl -s -X POST "$API/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' | python3 -c "import json,sys;print(json.load(sys.stdin)['jwt'])") + +# Use JWT for all subsequent requests +curl -H "Authorization: Bearer $JWT" "$API/api/v1/files/scan" +curl -H "Authorization: Bearer $JWT" "$API/api/v1/resource/tmdb" +``` + +JWT is short-lived (1 hour). When it expires, request a new one via login. + +--- + +### Using Session Cookie (Browser) + +Browser-based clients (Portal) get a session cookie automatically after login. The browser sends the cookie with every request—no manual header needed. + +```bash +# Login captures the session cookie from Set-Cookie header +curl -v -X POST "$API/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' 2>&1 | grep "Set-Cookie" + +# Browser automatically sends: Cookie: session_id= +# No manual header needed for subsequent requests +``` + +The session cookie is HttpOnly (not accessible from JavaScript) and SameSite=Strict (protected against CSRF). + +--- + +### Using Legacy API Key + +```bash +curl -H "X-API-Key: $KEY" "$API/api/v1/files/scan" + +# Also accepted via Bearer header (non-JWT format) or query parameter: +curl -H "Authorization: Bearer $KEY" "$API/api/v1/files/scan" +curl "$API/api/v1/files/scan?api_key=$KEY" +``` + +API keys are validated via SHA256 hash lookup in the database. They are long-lived (90 days) and intended for automation. + +### Obtaining an API Key (CLI) + +```bash +momentry api-key create "My API Key" --key-type user +``` + +--- + +### Logout + +```bash +# Logout using the session cookie (browser) +curl -X POST "$API/api/v1/auth/logout" \ + -H "Cookie: session_id=" +``` + +#### What logout does + +| Auth mode | Effect | +|-----------|--------| +| **Session Cookie** | Session deleted from database. Same cookie returns 401 on subsequent requests. | +| **JWT** | JWT remains valid until expiry. (JWT is stateless — logout adds JWT to a blacklist only if API key mode is used.) | +| **API Key** | API key remains valid. (Legacy keys are shared across sessions — revoking would break other clients.) | + +#### Example: full session lifecycle + +```bash +# 1. Login +SESSION_ID=$(curl -s -D - -X POST "$API/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' | grep "Set-Cookie" | sed 's/.*session_id=\([^;]*\).*/\1/') + +# 2. Use session (works) +curl -s -o /dev/null -w "HTTP %{http_code}\n" "$API/api/v1/resource/tmdb" \ + -H "Cookie: session_id=$SESSION_ID" +# → HTTP 200 + +# 3. Logout +curl -s -X POST "$API/api/v1/auth/logout" \ + -H "Cookie: session_id=$SESSION_ID" +# → {"success": true} + +# 4. Use session again (rejected) +curl -s -o /dev/null -w "HTTP %{http_code}\n" "$API/api/v1/resource/tmdb" \ + -H "Cookie: session_id=$SESSION_ID" +# → HTTP 401 +``` + +--- + +### Authentication Flow Summary + +``` +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=/ +├──────────────────┤ +│ 5. Return │ ← JWT + api_key + user info to client +└──────────────────┘ +``` + +``` +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 │ +└──────────────────────┘ +``` + +--- + +### Error Responses + +| HTTP | When | +|------|------| +| `401` | Missing or invalid authentication | +| `401` | Session expired or logged out | +| `401` | JWT expired | +| `401` | API key revoked or inactive | + +--- + +### Related + +- `POST /api/v1/resource/tmdb/check` — test authentication + TMDb API connectivity +- `GET /health/detailed` — view auth status (integrations section) diff --git a/docs_v1.0/doc_wasm/modules/02_health.md b/docs_v1.0/doc_wasm/modules/02_health.md new file mode 100644 index 0000000..46f7df0 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/02_health.md @@ -0,0 +1,147 @@ + + + + +## Health Check + +### `GET /health` + +**Auth**: Public +**Scope**: system-level + +Returns basic server health status — used by load balancers and monitoring. + +#### Example + +```bash +curl "$API/health" | jq '{status, version}' +``` + +#### Response (200) + +```json +{ + "status": "ok", + "version": "1.0.0", + "build_git_hash": "3a6c1865", + "build_timestamp": "2026-05-16T13:38:15Z", + "uptime_ms": 3015 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `status` | string | `ok` or `degraded` | +| `version` | string | Semver version | +| `build_git_hash` | string | Git commit hash | +| `build_timestamp` | string | Binary build time | +| `uptime_ms` | integer | Milliseconds since server start | + +--- + +### `GET /health/detailed` + +**Auth**: Required +**Scope**: system-level + +Returns full system health including each service status, resource utilization, pipeline readiness, schema migration status, identity file sync status, and external integrations. + +> Requires authentication (JWT, session cookie, or API key). The basic `/health` endpoint remains public for load balancer checks. + +#### Example + +```bash +curl "$API/health/detailed" | jq '{status, services, resources: {cpu: .resources.cpu_used_percent, memory: .resources.memory_used_percent}}' +``` + +#### Response (200) + +```json +{ + "status": "ok", + "version": "1.0.0", + "services": { + "postgres": {"status": "ok", "latency_ms": 3}, + "redis": {"status": "ok", "latency_ms": 1}, + "qdrant": {"status": "ok", "latency_ms": 5} + }, + "resources": { + "cpu_used_percent": 12.5, + "memory_available_mb": 32768, + "memory_used_percent": 31.7 + }, + "pipeline": { + "scripts_ready": true, + "scripts_count": 345, + "processors": { + "asr": true, + "yolo": true, + "face": true, + "pose": true, + "ocr": true, + "cut": true, + "scene": true, + "asrx": true, + "visual_chunk": true + }, + "models_ready": true, + "models_count": 42, + "scripts_integrity": {"matched": 332, "total": 345, "ok": false}, + "ffmpeg": true + }, + "schema": { + "table_exists": true, + "applied": [{"filename": "migrate_add_users_table.sql"}], + "required": [], + "ok": true + }, + "identities": { + "directory_exists": true, + "files_count": 3481, + "index_ok": true, + "db_count": 3481, + "synced": true + }, + "integrations": { + "tmdb": { + "api_key_configured": false, + "enabled": false, + "api_reachable": null + } + } +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `status` | string | `ok` if all essential services healthy | +| `services` | object | Per-service status (postgres, redis, qdrant) | +| `services.*.status` | string | `ok`, `error`, or `degraded` | +| `services.*.latency_ms` | int | Response time in milliseconds | +| `resources` | object | CPU, memory usage | +| `pipeline.scripts_ready` | boolean | Scripts directory accessible | +| `pipeline.scripts_count` | int | Number of Python processor scripts | +| `pipeline.processors` | object | Per-processor availability | +| `pipeline.models_ready` | boolean | Models directory accessible | +| `pipeline.scripts_integrity` | object | SHA256 checksum verification results | +| `schema.ok` | boolean | All required migrations applied | +| `identities.synced` | boolean | Identity file count matches DB count | +| `integrations.tmdb` | object | TMDB API key config and reachability | + +#### Health status rules + +| Condition | status | +|-----------|--------| +| All services ok | `ok` | +| Any service error | `degraded` | +| Postgres or Redis error | `degraded` (server still responds) | + +--- + +### Stats Endpoints + +| Method | Endpoint | Auth | Description | +|--------|----------|------|-------------| +| GET | `/api/v1/stats/sftpgo` | No | SFTPGo service status | diff --git a/docs_v1.0/doc_wasm/modules/03_register.md b/docs_v1.0/doc_wasm/modules/03_register.md new file mode 100644 index 0000000..9bae26f --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/03_register.md @@ -0,0 +1,184 @@ + + + + +## File Registration + +### `POST /api/v1/files/register` + +**Auth**: Required +**Scope**: file-level + +Register a video file for processing. Returns the file's metadata and UUID. + +**New in v0.1.2**: Registration now **automatically triggers the processing pipeline** — no need to call `POST /api/v1/file/:file_uuid/process` 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) + +If the file already exists (same content hash), returns the existing record with `already_exists: true`. + +#### Request Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `file_path` | string | Yes | — | Path to video file on disk | +| `pattern` | string | No | — | Regex pattern for batch register (requires `file_path` to be a directory) | +| `user_id` | integer | No | — | User ID to associate with registration | +| `content_hash` | string | No | — | Pre-computed SHA-256 hash (skips computation) | + +#### Example + +```bash +# Register a single file +curl -s -X POST "$API/api/v1/files/register" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_path": "/path/to/video.mp4"}' + +# Batch register files matching a pattern in a directory +curl -s -X POST "$API/api/v1/files/register" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}' +``` + +#### Response (200) + +```json +{ + "success": true, + "file_uuid": "3a6c1865...", + "file_name": "video.mp4", + "file_path": "/path/to/video.mp4", + "file_type": "video", + "duration": 120.5, + "width": 1920, + "height": 1080, + "fps": 24.0, + "total_frames": 2892, + "already_exists": false, + "message": "File registered successfully" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Always true on 200 | +| `file_uuid` | string | 32-char hex UUID of the registered file | +| `file_name` | string | File name (auto-renamed if name conflict) | +| `file_path` | string | Canonical path on disk | +| `file_type` | string | `"video"`, `"audio"`, or `"unknown"` | +| `duration` | float | Duration in seconds | +| `width` | integer | Video width in pixels | +| `height` | integer | Video height in pixels | +| `fps` | float | Frames per second | +| `total_frames` | integer | Total frame count | +| `already_exists` | boolean | True if same content was already registered | +| `message` | string | Human-readable status | + +#### Error Responses + +| HTTP | When | +|------|------| +| `401` | Missing or invalid API key | +| `400` | Invalid request body | +| `404` | File path does not exist | + +--- + +### `GET /api/v1/files/scan` + +**Auth**: Required +**Scope**: file-level + +Scan the filesystem directory and list all media files, showing which are registered, processing, or unregistered. + +#### Query Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `page` | integer | No | 1 | Page number (1-based) | +| `page_size` | integer | No | all | Items per page (alias: `limit`) | +| `limit` | integer | No | all | Max items (alias for `page_size`) | +| `pattern` | string | No | — | Regex filter on file name (e.g., `.*\\.mp4$`) | +| `sort_by` | string | No | `name` | Sort field: `name`, `size`, `modified`, `status` | +| `sort_order` | string | No | `asc` | Sort direction: `asc` or `desc` | + +#### Example + +```bash +# Full scan +curl -s "$API/api/v1/files/scan" -H "X-API-Key: $KEY" | jq '{total, registered_count, unregistered_count}' + +# Paginated (page 1, 5 per page) +curl -s "$API/api/v1/files/scan?page=1&page_size=5" -H "X-API-Key: $KEY" | jq '{page, total_pages, files: [.files[].file_name]}' + +# Regex filter: only mp4 files +curl -s "$API/api/v1/files/scan?pattern=.*\\.mp4$" -H "X-API-Key: $KEY" | jq '{filtered_total, files: [.files[].file_name]}' + +# Sort by file size (largest first) +curl -s "$API/api/v1/files/scan?sort_by=size&sort_order=desc&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, file_size}]' + +# Sort by modified time (most recent first) +curl -s "$API/api/v1/files/scan?sort_by=modified&sort_order=desc&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, modified_time}]' + +# Sort by status +curl -s "$API/api/v1/files/scan?sort_by=status&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, status}]' +``` + +#### Response (200) + +```json +{ + "files": [ + { + "file_name": "video.mp4", + "file_size": 12345678, + "is_registered": true, + "file_uuid": "3a6c1865...", + "status": "completed", + "registration_time": "2026-05-16T12:00:00Z", + "job_id": 42 + } + ], + "total": 107, + "filtered_total": 80, + "page": 1, + "page_size": 20, + "total_pages": 4, + "registered_count": 26, + "unregistered_count": 81 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `files` | array | Array of file info objects (paginated) | +| `files[].file_name` | string | File name | +| `files[].relative_path` | string | Path relative to scan root | +| `files[].file_path` | string | Absolute path on disk | +| `files[].file_size` | integer | File size in bytes | +| `files[].modified_time` | string | Last modified timestamp (ISO8601) | +| `files[].is_registered` | boolean | Whether file is registered in DB | +| `files[].file_uuid` | string | 32-char hex UUID (only if registered) | +| `files[].status` | string | `"completed"`, `"processing"`, `"registered"`, `"unregistered"`, or `null` | +| `files[].registration_time` | string | DB registration timestamp (only if registered) | +| `files[].job_id` | integer | Processing job ID (only if a job exists) | +| `total` | integer | Total files found on disk (unfiltered) | +| `filtered_total` | integer | Files matching regex filter | +| `page` | integer | Current page number | +| `page_size` | integer | Items per page | +| `total_pages` | integer | Total pages | +| `registered_count` | integer | Files registered in DB | +| `unregistered_count` | integer | Files not yet registered | + +#### Notes + +| Feature | Behavior | +|---------|----------| +| **Regex** | Case-insensitive (`(?i)` prefix auto-applied). Applied to `file_name`. | +| **Sort order** | Default (`sort_by=name`): registered files first, then alphabetically. `sort_by=status`: alphabetical by status string. | +| **Pagination** | `page_size` and `limit` are aliases. Default: show all results. | +| **Processing order** | `pattern` regex filter → `sort_by`/`sort_order` → `page`/`page_size` slice. | diff --git a/docs_v1.0/doc_wasm/modules/04_lookup.md b/docs_v1.0/doc_wasm/modules/04_lookup.md new file mode 100644 index 0000000..019c8af --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/04_lookup.md @@ -0,0 +1,138 @@ + + + + +## File Lookup + +### `GET /api/v1/files/lookup` + +**Auth**: Required +**Scope**: file-level + +Search registered files by file name. Performs a case-insensitive LIKE search on the file name column. Returns basic info about matching files. + +#### Query Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_name` | string | Yes | File name to search for (partial matches supported) | + +#### Example + +```bash +# Look up a specific file +curl -s "$API/api/v1/files/lookup?file_name=video.mp4" \ + -H "X-API-Key: $KEY" + +# Partial name search +curl -s "$API/api/v1/files/lookup?file_name=charade" \ + -H "X-API-Key: $KEY" | jq '.matches[].file_name' +``` + +#### Response (200) + +```json +{ + "file_name": "video.mp4", + "exists": true, + "matches": [ + { + "file_uuid": "a03485a40b2df2d3", + "file_name": "video.mp4", + "file_type": "video", + "status": "completed" + } + ], + "next_name": "video (2).mp4" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_name` | string | Searched name | +| `exists` | boolean | Exact name match exists | +| `matches` | array | Array of matching registered files | +| `matches[].file_uuid` | string | 32-char hex UUID | +| `matches[].file_name` | string | Registered file name | +| `matches[].file_type` | string | `"video"`, `"audio"`, or `null` | +| `matches[].status` | string | Registration/processing status | +| `next_name` | string | Suggested name for avoiding conflicts | + +--- + +## Unregister + +### `POST /api/v1/unregister` + +**Auth**: Required +**Scope**: file-level + +Delete a registered file from the system. Supports single file by UUID, or batch by directory + regex pattern. + +#### What gets deleted + +| Removed (default) | Not removed | +|---------|-------------| +| Database records (videos, chunks, embeddings, processor_results, pre_chunks) | The original source video file on disk | +| Processor output JSON files (`{uuid}.*.json`) — unless `delete_output_files: false` | Temp/working directories | +| In-memory cache entries | | +| MongoDB cached lists | | + +> ⚠️ Database deletion is **irreversible**. To keep output files, set `"delete_output_files": false`. + +#### Request Parameters + +At least one mode must be specified: either `file_uuid` alone, or `file_path` + `pattern` together. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `file_uuid` | string | * | — | Single file UUID to delete | +| `file_path` | string | * | — | Directory path (for batch delete) | +| `pattern` | string | * | — | Regex pattern (requires `file_path`) | +| `delete_output_files` | boolean | No | `true` | If `true`, also delete processor output JSON files (`{uuid}.*.json`). Set to `false` to keep them. | + +#### Example + +```bash +# Delete a single file by UUID (default: also deletes output JSON files) +curl -s -X POST "$API/api/v1/unregister" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_uuid": "'"$FILE_UUID"'"}' + +# Keep output JSON files, only delete DB records +curl -s -X POST "$API/api/v1/unregister" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_uuid": "'"$FILE_UUID"'", "delete_output_files": false}' + +# Batch delete all mp4 files in a directory +curl -s -X POST "$API/api/v1/unregister" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}' +``` + +#### Response (200) + +```json +{ + "success": true, + "file_uuid": "a03485a40b2df2d3", + "message": "Video unregistered successfully" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | True if deletion succeeded | +| `file_uuid` | string | UUID of the deleted file (single mode) | +| `message` | string | Human-readable status | + +#### Error Responses + +| HTTP | When | +|------|------| +| `400` | Neither `file_uuid` nor `file_path`+`pattern` provided | +| `404` | File UUID not found | +| `401` | Missing or invalid API key | diff --git a/docs_v1.0/doc_wasm/modules/05_process.md b/docs_v1.0/doc_wasm/modules/05_process.md new file mode 100644 index 0000000..9f9baf8 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/05_process.md @@ -0,0 +1,236 @@ + + + + +## Processing Pipeline + +### `POST /api/v1/file/:file_uuid/process` + +**Auth**: Required +**Scope**: file-level + +Trigger the processing pipeline for a registered file. Creates a monitor job that the worker picks up and processes sequentially. Returns immediately with the job info—processing runs asynchronously in the background. + +#### Request Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `processors` | string[] | No | all | Specific processors to run: `["cut","asr","asrx","yolo","ocr","face","pose","visual_chunk","story","5w1h"]` | +| `rules` | string[] | No | all | Rule names to apply (currently unused) | + +#### Example + +```bash +# Run all processors +curl -s -X POST "$API/api/v1/file/$FILE_UUID/process" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" -d '{}' + +# Run specific processors only +curl -s -X POST "$API/api/v1/file/$FILE_UUID/process" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"processors": ["asr", "face", "yolo"]}' +``` + +#### Response (200) + +```json +{ + "success": true, + "job_id": 42, + "file_uuid": "3a6c1865...", + "status": "processing", + "pids": [12345, 12346], + "message": "Processing triggered for video.mp4" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Always true on 200 | +| `job_id` | integer | Monitor job ID (for job tracking) | +| `file_uuid` | string | 32-char hex UUID of the file | +| `status` | string | `"processing"` | +| `pids` | integer[] | Process IDs of started processors | +| `message` | string | Human-readable status | + +#### Error Responses + +| HTTP | When | +|------|------| +| `404` | File UUID not found | +| `401` | Missing or invalid API key | + +--- + +### `GET /api/v1/file/:file_uuid/probe` + +**Auth**: Required +**Scope**: file-level + +Get ffprobe metadata for a registered file. Returns video/audio stream info, codec details, duration, resolution, and frame rate. + +#### Example + +```bash +curl -s "$API/api/v1/file/$FILE_UUID/probe" -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "file_name": "video.mp4", + "file_size": 794863677, + "duration": 120.5, + "width": 1920, + "height": 1080, + "fps": 24.0, + "total_frames": 2892, + "cached": true, + "format": { + "filename": "/path/to/video.mp4", + "format_name": "mov,mp4,m4a,3gp", + "duration": "120.5", + "size": "12345678", + "bit_rate": "819200" + }, + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_type": "video", + "width": 1920, + "height": 1080, + "r_frame_rate": "24/1", + "duration": "120.5" + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | 32-char hex UUID | +| `file_name` | string | File name | +| `file_size` | integer | File size in bytes (from filesystem) | +| `duration` | float | Duration in seconds | +| `width` | integer | Video width in pixels | +| `height` | integer | Video height in pixels | +| `fps` | float | Frames per second | +| `total_frames` | integer | Estimated total frames | +| `cached` | boolean | True if result was from cached probe JSON | +| `format` | object | Container format info (ffprobe format section) | +| `streams` | array | Array of stream info objects | + +--- + +### `GET /api/v1/progress/:file_uuid` + +**Auth**: Required +**Scope**: file-level + +Get real-time processing progress for a file via Redis pub/sub. Includes per-processor status, current/total frames, ETA, and system resource stats. + +#### Pipeline Order + +| Order | Processor | Dependencies | Description | +|-------|-----------|-------------|-------------| +| 1 | `cut` | — | Scene detection | +| 2 | `asr` | cut | Speech-to-text (per scene) | +| 3 | `asrx` | asr | Speaker diarization | +| 4 | `yolo` | — | Object detection | +| 5 | `ocr` | — | Text recognition | +| 6 | `face` | — | Face detection & embedding | +| 7 | `pose` | — | Pose estimation | +| 8 | `visual_chunk` | yolo | Visual scene chunks | +| 9 | `story` | asr, asrx, cut, yolo, face | Scene summaries (template) | +| 10 | `5w1h` | story | 5W1H analysis (Gemma4 LLM) | + +All processors except `story` and `5w1h` run concurrently when their dependencies are met. Story and 5W1H run sequentially after their prerequisites. + +#### Example + +```bash +curl -s "$API/api/v1/progress/$FILE_UUID" -H "X-API-Key: $KEY" | jq '{overall_progress, processors: [.processors[] | {processor_type, status}]}' +``` + +#### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "overall_progress": 71, + "cpu_percent": 45.2, + "gpu_percent": 30.1, + "memory_percent": 62.4, + "processors": [ + {"processor_type": "asr", "status": "complete", "progress": 100}, + {"processor_type": "yolo", "status": "running", "progress": 65}, + {"processor_type": "face", "status": "pending", "progress": 0} + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | 32-char hex UUID | +| `overall_progress` | integer | Overall progress percentage (0–100) | +| `processors` | array | Per-processor status list | +| `processors[].processor_type` | string | Processor name (`asr`, `cut`, `yolo`, etc.) | +| `processors[].status` | string | `"pending"`, `"running"`, `"complete"`, or `"failed"` | +| `processors[].progress` | integer | Per-processor progress (0–100) | +| `processors[].eta_seconds` | integer | Estimated seconds remaining (running processors) | +| `processors[].current` | integer | Current frame count | +| `processors[].total` | integer | Total frame count | +| `cpu_percent` | float | Current CPU usage | +| `gpu_percent` | float | Current GPU utilization | +| `memory_percent` | float | Current memory usage | + +--- + +### `GET /api/v1/jobs` + +**Auth**: Required +**Scope**: system-level + +List all processing jobs (monitor jobs) in the system. Shows job status, which file each job is processing, and current processor info. + +#### Example + +```bash +curl -s "$API/api/v1/jobs" -H "X-API-Key: $KEY" | jq '{count, jobs: [.jobs[] | {uuid, status}]}' +``` + +#### Response (200) + +```json +{ + "jobs": [ + { + "id": 42, + "uuid": "3a6c1865...", + "status": "running", + "current_processor": "yolo", + "created_at": "2026-05-16T12:00:00Z", + "started_at": "2026-05-16T12:01:00Z" + } + ], + "count": 15, + "page": 1, + "page_size": 20 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `jobs` | array | Array of job info objects | +| `jobs[].id` | integer | Job ID | +| `jobs[].uuid` | string | File UUID being processed | +| `jobs[].status` | string | `"pending"`, `"running"`, `"completed"`, `"failed"` | +| `jobs[].current_processor` | string | Currently active processor, or null | +| `count` | integer | Total job count | +| `page` | integer | Current page number | +| `page_size` | integer | Jobs per page | diff --git a/docs_v1.0/doc_wasm/modules/06_search.md b/docs_v1.0/doc_wasm/modules/06_search.md new file mode 100644 index 0000000..e5b13c3 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/06_search.md @@ -0,0 +1,145 @@ + + + + +## Search APIs + +### `POST /api/v1/search/smart` + +**Auth**: Required +**Scope**: 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. + +#### Request Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `file_uuid` | string | Yes | — | File UUID to search within | +| `query` | string | Yes | — | Search text | +| `limit` | integer | No | 5 | Max results to return | +| `page` | integer | No | 1 | Page number | +| `page_size` | integer | No | 5 | Items per page | + +#### Example + +```bash +curl -s -X POST "$API/api/v1/search/smart" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $JWT" \ + -d '{"file_uuid": "'"$FILE_UUID"'", "query": "Audrey Hepburn"}' +``` + +#### Response (200) + +```json +{ + "query": "Audrey Hepburn", + "results": [ + { + "parent_id": 1087822, + "scene_order": 1087822, + "start_frame": 104438, + "end_frame": 104538, + "fps": 24.0, + "start_time": 4351.6, + "end_time": 4355.76, + "summary": "[4352s-4356s, 4s] Cast: Audrey Hepburn. Total: 2 lines, 10 words. Speakers: Audrey Hepburn (2 lines)", + "similarity": 0.67 + } + ], + "page": 1, + "page_size": 5, + "strategy": "semantic_vector_search" +} +``` + +--- + +### `POST /api/v1/search/universal` + +**Auth**: Required +**Scope**: file-level + +Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`. + +#### Request Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `query` | string | Yes | — | Search text | +| `file_uuid` | string | No | — | Restrict to specific file | +| `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 + +```bash +curl -s -X POST "$API/api/v1/search/universal" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $JWT" \ + -d '{"file_uuid": "'"$FILE_UUID"'", "query": "Cary Grant"}' +``` + +#### Response (200) + +```json +{ + "results": [ + { + "type": "chunk", + "chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2", + "chunk_type": "story_child", + "start_frame": 5103, + "end_frame": 5127, + "start_time": 212.64, + "end_time": 213.64, + "text": "[213s-214s] Cary Grant: \"Olá!\"", + "score": 0.9 + } + ], + "total": 20, + "took_ms": 18 +} +``` + +--- + +### `POST /api/v1/search/frames` + +**Auth**: Required +**Scope**: file-level + +Search face detection frames by identity name or trace ID. + +--- + +### `POST /api/v1/search/identity_text` + +**Auth**: Required +**Scope**: file-level + +Search text chunks spoken by a specific identity. + +--- + +### Visual Search + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/v1/search/visual` | Search visual chunks | +| POST | `/api/v1/search/visual/class` | Search by object class | +| POST | `/api/v1/search/visual/density` | Search by object density | +| POST | `/api/v1/search/visual/combination` | Search by object combination | +| POST | `/api/v1/search/visual/stats` | Visual chunk statistics | + +#### Embedding Model + +| Detail | Value | +|--------|-------| +| **Model** | EmbeddingGemma-300m | +| **Endpoint** | `POST /api/v1/embeddings` on port 11436 | +| **Dimension** | 768 | +| **Storage** | pgvector (`chunk.embedding` column) | diff --git a/docs_v1.0/doc_wasm/modules/07_identity.md b/docs_v1.0/doc_wasm/modules/07_identity.md new file mode 100644 index 0000000..4b8e3ef --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/07_identity.md @@ -0,0 +1,333 @@ + + + + +## Global Identities + +### `GET /api/v1/identities` + +**Auth**: Required +**Scope**: identity-level + +List all registered identities with pagination. + +#### Example + +```bash +curl -s "$API/api/v1/identities?page=1&page_size=20" -H "X-API-Key: $KEY" | jq '{count, identities: [.identities[] | {name}]}' +``` + +--- + +### `GET /api/v1/identity/:identity_uuid` + +**Auth**: Required +**Scope**: identity-level + +Get detailed information for a specific identity, including metadata and TMDb references. + +#### Example + +```bash +curl -s "$API/api/v1/identity/$IDENTITY_UUID" -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "success": true, + "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4", + "name": "Cary Grant", + "identity_type": "people", + "source": "tmdb", + "status": "confirmed", + "tmdb_id": 112, + "tmdb_profile": "{output}/identities/{identity_uuid}/profile.jpg", + "metadata": {}, + "reference_data": {}, + "created_at": "2026-05-16T12:00:00Z", + "updated_at": null +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `identity_uuid` | string | Identity identifier | +| `name` | string | Identity name | +| `identity_type` | string | `"people"` or null | +| `source` | string | `.json`, `auto`, `tmdb`, `user_defined`, or `merged` | +| `status` | string | `"confirmed"`, `"pending"`, or `"inactive"` | +| `tmdb_id` | integer | TMDb person ID (only if source = tmdb) | +| `tmdb_profile` | string | Local profile image path (`{output}/identities/{uuid}/profile.jpg`) | +| `metadata` | object | Metadata JSON (tmdb_character, cast_order, etc.) | +| `created_at` | string | Creation timestamp | + +--- + +### `DELETE /api/v1/identity/:identity_uuid` + +**Auth**: Required +**Scope**: identity-level + +Delete an identity permanently. + +--- + +### `GET /api/v1/identity/:identity_uuid/files` + +**Auth**: Required +**Scope**: identity-level + +Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range. + +#### Example + +```bash +curl -s "$API/api/v1/identity/$IDENTITY_UUID/files" -H "X-API-Key: $KEY" +``` + +--- + +### `GET /api/v1/identity/:identity_uuid/faces` + +**Auth**: Required +**Scope**: identity-level + +Get all face detection records associated with this identity. + +#### Example + +```bash +curl -s "$API/api/v1/identity/$IDENTITY_UUID/faces" -H "X-API-Key: $KEY" +``` + +| 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 | + +--- + +### `GET /api/v1/identity/:identity_uuid/chunks` + +**Auth**: Required +**Scope**: identity-level + +Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said. + +#### Example + +```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}`) | + +#### 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"}' +``` + +--- + +### `POST /api/v1/identity/:identity_uuid/unbind` + +**Auth**: Required +**Scope**: identity-level + +Unbind a face detection from an identity. Removes the identity association from the face record. + +--- + +### `GET /api/v1/identities/search` + +**Auth**: Required +**Scope**: identity-level + +Search identities by name (ILIKE search). Returns matching identity records. + +#### Example + +```bash +curl -s "$API/api/v1/identities/search?q=Cary" -H "X-API-Key: $KEY" +``` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Identity name | +| `source` | string | Identity source | +| `tmdb_id` | integer | TMDb ID (if source = tmdb) | +| `file_uuid` | string | Associated file | + +--- + +--- + +### `POST /api/v1/identity/upload` + +**Auth**: Required +**Scope**: identity-level + +Upload an identity.json file to create or update an identity. Accepts the same format as the identity.json files stored on disk. + +If an identity with the same `name` already exists, it will be updated with the new values. + +#### Request + +The request body is an `IdentityFile` object: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `identity_uuid` | string | Yes | Identity identifier | +| `name` | string | Yes | Identity display name | +| `identity_type` | string | No | `"people"` or null | +| `source` | string | No | `.json`, `auto`, `tmdb`, `user_defined`, or `merged` | +| `status` | string | No | `"confirmed"`, `"pending"`, or `"inactive"` | +| `tmdb_id` | integer | No | TMDb person ID | +| `tmdb_profile` | string | No | TMDb profile image URL | +| `metadata` | object | No | Arbitrary metadata JSON | +| `file_bindings` | array | No | Array of `{ file_uuid, trace_ids, face_count }` (informational) | + +#### Example + +```bash +curl -s -X POST "$API/api/v1/identity/upload" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "version": 1, + "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4", + "name": "Cary Grant", + "identity_type": "people", + "source": ".json", + "status": "confirmed", + "metadata": {}, + "file_bindings": [] + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4", + "name": "Cary Grant", + "message": "Identity uploaded successfully" +} +``` + +--- + +--- + +### `POST /api/v1/identity/:identity_uuid/profile-image` + +**Auth**: Required +**Scope**: identity-level + +Upload a profile image (JPEG or PNG) for an identity. The image is saved to `{output}/identities/{uuid}/profile.{ext}`. + +Uses `multipart/form-data` with field name `image`. + +#### Example + +```bash +curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/profile-image" \ + -H "X-API-Key: $KEY" \ + -F "image=@/path/to/photo.jpg" +``` + +#### Response (200) + +```json +{ + "success": true, + "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4", + "path": "/path/to/output/identities/.../profile.jpg", + "message": "Profile image saved: profile.jpg" +} +``` + +#### Error Responses + +| HTTP | When | +|------|------| +| `400` | Missing image field or unsupported format | +| `404` | Identity not found | +| `415` | Unsupported image type (use JPEG or PNG) | + +--- + +### `GET /api/v1/identity/:identity_uuid/profile-image` + +**Auth**: Required +**Scope**: identity-level + +Retrieve the profile image for an identity. Returns the raw image data with appropriate Content-Type header. + +```bash +curl -s "$API/api/v1/identity/$IDENTITY_UUID/profile-image" \ + -H "X-API-Key: $KEY" -o profile.jpg +``` + +| Response Header | Value | +|----------------|-------| +| `content-type` | `image/jpeg` or `image/png` | + + diff --git a/docs_v1.0/doc_wasm/modules/08_identity_agent.md b/docs_v1.0/doc_wasm/modules/08_identity_agent.md new file mode 100644 index 0000000..f9c86e0 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/08_identity_agent.md @@ -0,0 +1,65 @@ + + + + +## Identity Agent + +### `POST /api/v1/agents/identity/match-from-photo` + +**Auth**: Required +**Scope**: file-level + +Upload a face photo to match against known identities. Detects face via InsightFace, extracts 512D embedding via CoreML FaceNet, then searches pgvector for the closest identity. + +#### Request + +`multipart/form-data` with field `image` (JPEG/PNG) and optional `file_uuid`. + +#### Example + +```bash +curl -s -X POST "$API/api/v1/agents/identity/match-from-photo" \ + -H "Authorization: Bearer $JWT" \ + -F "image=@/path/to/face.jpg" \ + -F "file_uuid=$FILE_UUID" +``` + +#### Response (200) + +```json +{ + "success": true, + "matches": [ + { + "identity_uuid": "a9a90105...", + "name": "Cary Grant", + "similarity": 0.87 + } + ] +} +``` + +--- + +### `POST /api/v1/agents/identity/match-from-trace` + +**Auth**: Required +**Scope**: file-level + +Match a face trace (tracked face across frames) against known identities. Samples 3 angles from the trace, generates embeddings, and searches pgvector. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File containing the trace | +| `trace_id` | integer | Yes | Face trace ID to match | + +#### Example + +```bash +curl -s -X POST "$API/api/v1/agents/identity/match-from-trace" \ + -H "Authorization: Bearer $JWT" \ + -H "Content-Type: application/json" \ + -d '{"file_uuid": "'"$FILE_UUID"'", "trace_id": 10}' +``` diff --git a/docs_v1.0/doc_wasm/modules/08_media.md b/docs_v1.0/doc_wasm/modules/08_media.md new file mode 100644 index 0000000..763747d --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/08_media.md @@ -0,0 +1,146 @@ + + + + +## Video Streaming & Frame Extraction + +All video streaming endpoints support the following common query parameters: + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `mode` | string | No | `normal` | `normal` or `debug` (draws detection overlays) | +| `audio` | string | No | `on` | `on` or `off` | + +--- + +### `GET /api/v1/file/:file_uuid/video` + +Stream the full video file with range support for seeking. + +**Auth**: Required +**Scope**: file-level + +#### Response + +- **200**: Video stream (`Content-Type` based on file extension) +- **206**: Partial content (range request) +- Supports `Range` header for seeking + +--- + +### `GET /api/v1/file/:file_uuid/trace/:trace_id/video` + +Stream video with highlights for a specific face trace (follows a single person across frames with bounding box overlay). + +**Auth**: Required +**Scope**: file-level + +--- + +### `GET /api/v1/file/:file_uuid/video/bbox` + +Stream video with bounding box overlay for all detected objects/faces. + +**Auth**: Required +**Scope**: file-level + +Uses a built-in 5×7 bitmap font renderer to draw labels directly on video frames via FFmpeg `drawtext` filter. + +--- + +### `GET /api/v1/file/:file_uuid/thumbnail` + +Extract a single frame from a video as JPEG image. Uses FFmpeg `select` filter. + +**Auth**: Required +**Scope**: file-level + +#### Query Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `frame` | integer | Yes | — | Zero-based frame number to extract | +| `x` | integer | No | — | Crop start X (left edge). Requires `y`, `w`, `h`. | +| `y` | integer | No | — | Crop start Y (top edge). Requires `x`, `w`, `h`. | +| `w` | integer | No | — | Crop width in pixels. Requires `x`, `y`, `h`. | +| `h` | integer | No | — | Crop height in pixels. Requires `x`, `y`, `w`. | + +All four crop params (`x`, `y`, `w`, `h`) must be provided together or omitted. + +#### Example + +```bash +# Extract frame 1000 (full frame) +curl -s "$API/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000" \ + -H "Authorization: Bearer $JWT" -o frame_1000.jpg + +# Extract and crop face region (x=320, y=240, w=160, h=160) +curl -s "$API/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000&x=320&y=240&w=160&h=160" \ + -H "Authorization: Bearer $JWT" -o face_crop.jpg +``` + +#### Response + +- **200**: `image/jpeg` binary data +- **404**: File not found +- **500**: FFmpeg error (e.g., frame number exceeds video duration) + +### `GET /api/v1/file/:file_uuid/clip` + +Extract a video clip (time range) as MPEG-TS stream. Uses FFmpeg `-ss` fast seek. + +**Auth**: Required +**Scope**: file-level + +#### Query Parameters + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `start_frame` | integer | No* | — | Start frame (zero-based). **Frame-accurate** — use this for precision. | +| `end_frame` | integer | No* | — | End frame (zero-based, inclusive). Requires `start_frame`. | +| `start_time` | float | No* | — | Start time in seconds. Approximate (FPS-dependent). Fallback if frames not given. | +| `end_time` | float | No* | — | End time in seconds. Approximate (FPS-dependent). Fallback if frames not given. | +| `fps` | float | No | video FPS | Override frames-per-second for frame↔time calculation. Defaults to video's detected FPS. | +| `mode` | string | No | `normal` | `normal` or `debug` (draws "CLIP" overlay) | +| `audio` | string | No | `on` | `on` or `off` | + +Either (`start_frame`+`end_frame`) OR (`start_time`+`end_time`) must be provided. + +#### Example + +```bash +# Clip by frame range (primary) +curl -s "$API/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/clip?start_frame=0&end_frame=47" \ + -H "Authorization: Bearer $JWT" -o clip.ts + +# Clip by time range (fallback) +curl -s "$API/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/clip?start_time=30&end_time=45" \ + -H "Authorization: Bearer $JWT" -o clip.ts +``` + +#### Response + +- **200**: `video/mp2t` MPEG-TS stream +- **400**: Missing/invalid range parameters +- **404**: File not found +- **500**: FFmpeg error + +#### Technical Notes + +| Detail | Value | +|--------|-------| +| **Backend** | FFmpeg (`ffmpeg-full`) | +| **Seek** | `-ss` before `-i` (fast keyframe seek) | +| **Format** | MPEG-TS (`mpegts` muxer, pipe-safe) | +| **Codec** | H.264 + AAC | +| **Cache** | `Cache-Control: public, max-age=86400` (24h) | + +--- + +| Detail | Value | +|--------|-------| +| **Backend** | FFmpeg (`ffmpeg-full`) | +| **Filter** | `select=eq(n\,FRAME)` to select frame, optional `crop=W:H:X:Y` | +| **Output** | Single JPEG via pipe (`image2pipe`, `mjpeg` codec) | +| **Cache** | `Cache-Control: public, max-age=86400` (24h) | +| **Frame number** | Zero-based (`frame=0` = first frame of video) | diff --git a/docs_v1.0/doc_wasm/modules/09_tmdb.md b/docs_v1.0/doc_wasm/modules/09_tmdb.md new file mode 100644 index 0000000..7ea3f27 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/09_tmdb.md @@ -0,0 +1,109 @@ + + + + +## TMDb Enrichment + +> **Offline operation**: TMDb prefetch now checks local identity files first (`identities/_index.json` + `*.tmdb.json`). +> If local files exist, no external API call is made. Internet is only needed for initial data seeding. + +### Overview + +TMDb enrichment is an optional identity enrichment step that can be run after Pipeline face detection completes. The workflow is: + +1. **Prefetch** (requires internet): Download movie cast data from TMDb API → cache to `{file_uuid}.tmdb.json` +2. **Probe**: Read local cache → create identities for **all** cast members (`source='tmdb'`) + save `identity.json` + download profile image to `{OUTPUT}/identities/{uuid}/profile.jpg` +3. **Match**: The worker automatically matches video faces against TMDb identities when `MOMENTRY_TMDB_PROBE_ENABLED=true` + +### `POST /api/v1/agents/tmdb/prefetch` + +**Auth**: Required +**Scope**: file-level + +Fetch TMDb cast data for a registered file and cache it locally. This is the only step requiring internet access. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID to enrich | + +#### Example + +```bash +curl -s -X POST "$API/api/v1/agents/tmdb/prefetch" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"file_uuid": "'"$FILE_UUID"'"}' +``` + +#### Response (200) + +```json +{"success": true, "file_uuid": "...", "cache_path": "/output/...tmdb.json"} +``` + +### `POST /api/v1/file/:file_uuid/tmdb-probe` + +**Auth**: Required +**Scope**: file-level + +Read local TMDb cache and create/update identities. Requires prefetch to have been run first. + +#### Example + +```bash +curl -s -X POST "$API/api/v1/file/$FILE_UUID/tmdb-probe" \ + -H "X-API-Key: $KEY" | jq '{identities_created, movie_title}' +``` + +#### Response (200 — identities created) + +```json +{"success": true, "identities_created": 15, "movie_title": "Charade"} +``` + +#### Response (200 — no cache) + +```json +{"success": false, "message": "No TMDb cache found. Run tmdb-prefetch first."} +``` + +### `GET /api/v1/resource/tmdb` + +**Auth**: Required +**Scope**: system-level + +View TMDb resource status including configuration, identity counts, and cache file count. + +#### Example + +```bash +curl -s "$API/api/v1/resource/tmdb" -H "X-API-Key: $KEY" \ + | jq '{identities_seeded, cache_files}' +``` + +### `POST /api/v1/resource/tmdb/check` + +**Auth**: Required +**Scope**: system-level + +Ping the TMDb API to verify connectivity and measure latency. + +#### Example + +```bash +curl -s -X POST "$API/api/v1/resource/tmdb/check" \ + -H "X-API-Key: $KEY" | jq '.status' +``` + +#### Response + +```json +{ + "api_key_configured": true, + "enabled": false, + "api_reachable": true, + "api_latency_ms": 120 +} +``` diff --git a/docs_v1.0/doc_wasm/modules/10_pipeline.md b/docs_v1.0/doc_wasm/modules/10_pipeline.md new file mode 100644 index 0000000..05c1f4a --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/10_pipeline.md @@ -0,0 +1,178 @@ + + + + +## Pipeline + +### Dependency Graph + +```mermaid +flowchart TB + subgraph Processors["10 Processors"] + Cut[Cut] --> ASR[ASR] + ASR --> ASRX[ASRX] + ASRX --> Story[Story] + Cut --> Story + YOLO[YOLO] --> VisualChunk[VisualChunk] + VisualChunk --> Story + Face[Face] --> Story + Story --> FiveW1H[5W1H] + OCR[OCR] + Pose[Pose] + end + + subgraph Ingestion["入庫 (Post-Processing)"] + ASR --> Rule1[Rule 1 Sentence] + ASRX --> Rule1 + Rule1 --> Vectorize[Auto-Vectorize] + Rule1 --> Phase1[Phase 1 Pack] + + Cut --> Rule3[Rule 3 Scene] + ASR --> Rule3 + + Face --> Trace[Face Trace] + Trace --> Qdrant[Qdrant Sync] + Trace --> TraceChunks[Trace Chunks] + Trace --> TKG[TKG Builder] + + Face --> TMDbMatch[TMDb Match] + Face --> SceneMeta[Scene Metadata] + YOLO --> SceneMeta + Face --> IdentityAgent[Identity Agent] + ASRX --> IdentityAgent + + Cut --> Agent5W1H[5W1H Agent] + ASR --> Agent5W1H + Agent5W1H --> Phase2[Phase 2 Pack] + end + + style Processors fill:#1a1a2e,stroke:#e94560 + style Ingestion fill:#16213e,stroke:#0f3460 +``` + +### Pipeline Completion Flow + +The pipeline is **not complete** until both the 10 processors AND the 入庫 (ingestion) steps have finished. The worker polls every 3 seconds and only marks the job as `completed` when all ingestion steps verify OK. + +``` +10 processors done + ↓ (job status stays "running") +Algorithm 1 Trigger: Rule 1 + Vectorize + Phase 1 Pack + ↓ (job runs in parallel) +Algorithm 2 Trigger: Face Trace → TKG, Scene Metadata, Identity Agent, 5W1H Agent + ↓ (poll checks every 3s) +Ingestion verification: rule1 ✓ vectorize ✓ rule3 ✓ face_trace ✓ tkg ✓ scene_meta ✓ 5w1h ✓ + ↓ +job status = "completed" +``` + +### 10 Processor Stages + +| # | Processor | Depends On | Description | +|---|-----------|------------|-------------| +| 1 | `Cut` | — | Scene boundary detection (PySceneDetect) | +| 2 | `ASR` | Cut | Automatic speech recognition (faster-whisper) | +| 3 | `ASRX` | ASR | Speaker diarization + ASR refinement | +| 4 | `YOLO` | — | Object detection (YOLOv8) | +| 5 | `OCR` | — | Optical character recognition | +| 6 | `Face` | — | Face detection + recognition (InsightFace + CoreML) | +| 7 | `Pose` | — | Pose estimation | +| 8 | `VisualChunk` | YOLO | Visual object chunking | +| 9 | `Story` | ASRX + Cut + YOLO + Face | Narrative scene summarization (LLM, with embedding) | +| 10 | `5W1H` | Story | Who/What/When/Where/Why extraction (LLM, with embedding) | + +### 入庫 (Post-Processing / Ingestion) + +These steps run after the 10 processors and are **required for pipeline completion**. The worker checks all of them before marking the job as done. + +| # | Step | Triggers When | Verification | +|---|------|--------------|-------------| +| 1 | **Rule 1 Sentence Chunking** | ASR + ASRX done | `chunk` table has rows with `chunk_type = 'sentence'` | +| 2 | **Auto-Vectorize** | Rule 1 done | `chunk.embedding` IS NOT NULL for sentence chunks | +| 3 | **Phase 1 Pack** | Rule 1 done | `release_pack.py --phase 1` executed | +| 4 | **Rule 3 Scene Chunking** | All 10 processors done + Cut + ASR | `chunk` table has rows with `chunk_type = 'cut'` | +| 5 | **Face Trace** | All 10 processors done + Face | `face_detections.trace_id` IS NOT NULL | +| 6 | **Qdrant Face Sync** | Face Trace done | Qdrant face_embedding collection populated | +| 7 | **Trace Chunks** | Face Trace done | `chunk` table has rows with `chunk_type = 'trace'` | +| 8 | **TKG Builder** | Face Trace done | `tkg_nodes` + `tkg_edges` tables have rows | +| 9 | **TMDb Face Matching** | TMDb enabled + Face done | `face_detections.identity_id` IS NOT NULL | +| 10 | **Heuristic Scene Metadata** | Face + YOLO done | `{file_uuid}.scene_meta.json` exists on disk | +| 11 | **Identity Agent** | Face + ASRX done | `identities` with `source = 'identity_agent'` | +| 12 | **5W1H Agent** | Cut + ASR done | `chunk.summary_text` IS NOT NULL for cut chunks | +| 13 | **Release Pack** | 5W1H Agent done | `release_pack.py --phase 2` executed | + +### Ingestion Status + +Check real-time ingestion status for a file: + +```bash +curl "$API/api/v1/stats/ingestion-status/{file_uuid}" +``` + +Returns per-step `done` / `pending` status with detail counts. + +#### Example + +```bash +curl "http://localhost:3003/api/v1/stats/ingestion-status/bd80fec9c42afb0307eb28f22c64c76a" | jq '.steps[] | {name, status, detail}' +``` + +#### Response + +```json +{ + "file_uuid": "bd80fec9c42afb0307eb28f22c64c76a", + "steps": [ + { "name": "rule1_sentence", "status": "pending", "detail": "0 sentence chunks" }, + { "name": "auto_vectorize", "status": "pending", "detail": "0 embedded" }, + { "name": "rule3_scene", "status": "pending", "detail": "0 scene chunks" }, + { "name": "face_trace", "status": "pending", "detail": "0 traces" }, + { "name": "trace_chunks", "status": "pending", "detail": "0 trace chunks" }, + { "name": "tkg", "status": "pending", "detail": "0 nodes, 0 edges" }, + { "name": "identity_match", "status": "pending", "detail": "0 identities" }, + { "name": "scene_metadata", "status": "pending", "detail": null }, + { "name": "5w1h", "status": "pending", "detail": "0 scenes with 5W1H" } + ] +} +``` + +### Stats Endpoints + +| Method | Endpoint | Auth | Description | +|--------|----------|------|-------------| +| GET | `/api/v1/stats/sftpgo` | No | SFTPGo service status | +| GET | `/api/v1/stats/ingestion-status/:file_uuid` | No | Per-file ingestion checklist | + +### Configuration + +### `POST /api/v1/config/cache` + +**Auth**: Required +**Scope**: system-level + +Toggle the Redis cache on or off. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `enabled` | boolean | Yes | `true` to enable, `false` to disable | + +#### Example + +```bash +curl -s -X POST "$API/api/v1/config/cache" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $KEY" \ + -d '{"enabled": false}' +``` + +### Unmounted Routes + +The following routes are defined in source code but are **NOT** currently mounted in the router: + +| Endpoint | Source file | +|----------|-------------| +| `/api/v1/search/persons` | `universal_search.rs` (not mounted) | +| `/api/v1/who` | `who.rs` | +| `/api/v1/who/candidates` | `who.rs` | diff --git a/docs_v1.0/doc_wasm/modules/11_error_codes.md b/docs_v1.0/doc_wasm/modules/11_error_codes.md new file mode 100644 index 0000000..15f2dad --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/11_error_codes.md @@ -0,0 +1,57 @@ + + + + +## Error Response Format + +All API errors follow this JSON structure: + +```json +{ + "success": false, + "error": { + "code": "E001_NOT_FOUND", + "message": "Resource not found", + "details": {"resource": "file_uuid", "value": "abc"} + } +} +``` + +## Error Code List + +### Generic Errors (E0xx) + +| Code | HTTP | Description | +|------|------|-------------| +| `E001_NOT_FOUND` | 404 | Resource not found (file, identity, chunk) | +| `E002_DUPLICATE` | 409 | Resource already exists | +| `E003_VALIDATION` | 400 | Request parameter validation failed | +| `E004_UNAUTHORIZED` | 401 | Invalid API key or token | +| `E005_INTERNAL` | 500 | Internal server error | + +### Processor Errors (E1xx) + +| Code | HTTP | Description | +|------|------|-------------| +| `E101_PROCESSOR_FAIL` | 500 | Python script execution failed | +| `E102_TIMEOUT` | 504 | Processing timeout | +| `E103_RESUME_FAIL` | 500 | Resume failed (checkpoint not found) | +| `E104_NO_VIDEO` | 400 | Video file path not found | + +### Identity Errors (E2xx) + +| Code | HTTP | Description | +|------|------|-------------| +| `E201_FACE_NOT_FOUND` | 404 | Face detection not found | +| `E202_MERGE_CONFLICT` | 409 | Identity merge conflict | +| `E203_CANDIDATE_EMPTY` | 404 | No candidates available for confirmation | + +### TMDb Errors (E3xx) + +| Code | HTTP | Description | +|------|------|-------------| +| `E301_TMDB_NO_KEY` | 400 | `TMDB_API_KEY` environment variable not set | +| `E302_TMDB_UNREACHABLE` | 502 | TMDb API unreachable or timed out | +| `E303_TMDB_CACHE_NOT_FOUND` | 200 | No local TMDb cache; run prefetch first | +| `E304_TMDB_PROBE_FAILED` | 500 | TMDb probe execution failed | +| `E305_TMDB_MOVIE_NOT_FOUND` | 404 | No matching TMDb movie found from filename | diff --git a/docs_v1.0/doc_wasm/modules/12_agent.md b/docs_v1.0/doc_wasm/modules/12_agent.md new file mode 100644 index 0000000..e17fbc3 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/12_agent.md @@ -0,0 +1,118 @@ +# Agent Endpoints + +Agent endpoints provide AI-powered capabilities including translation, identity analysis, and 5W1H extraction. + +## POST /api/v1/agents/translate + +Translate text between languages using Gemma4 (llama.cpp, port 8082). + +### Request + +```json +{ + "text": "Hello, welcome to Momentry Core.", + "target_language": "Traditional Chinese", + "source_language": "English" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `text` | string | ✅ | Text to translate | +| `target_language` | string | ✅ | Target language name (e.g. "Traditional Chinese", "Japanese") | +| `source_language` | string | ❌ | Source language (default: "auto") | + +### Response + +```json +{ + "success": true, + "translated_text": "您好,歡迎使用 Momentry Core。", + "source_language_detected": "English", + "model_used": "google_gemma-4-26B-A4B-it-Q5_K_M.gguf" +} +``` + +### Supported Language Pairs (tested) + +| Source | Target | Quality | +|--------|--------|---------| +| English | Traditional Chinese | ✅ | +| English | Japanese | ✅ | +| Chinese | English | ✅ | +| English | French | ✅ | +| Chinese | Japanese | ✅ | + +### Model + +- **Model**: Gemma4 26B (Q5_K_M) +- **Engine**: llama.cpp at `localhost:8082` +- **Endpoint**: `/v1/chat/completions` (OpenAI-compatible) +- **Temperature**: 0.1 +- **Max tokens**: 1024 + +### Errors + +| Status | Condition | +|--------|-----------| +| 500 | LLM unreachable or response parse failure | +| 401 | Missing/invalid auth | + +--- + +## POST /api/v1/agents/5w1h/analyze + +Extract 5W1H (Who, What, When, Where, Why, How) from a scene. Uses Gemma4 LLM on port 8082. + +### Request + +```json +{ + "file_uuid": "3abeee81d94597629ed8cb943f182e94", + "scene_id": 42 +} +``` + +### Response + +```json +{ + "success": true, + "5w1h": { + "who": ["Cary Grant"], + "what": ["discussing plans"], + "when": ["1963"], + "where": ["Paris"], + "why": ["vacation"], + "how": ["in person"] + } +} +``` + +## POST /api/v1/agents/5w1h/batch + +Batch analyze all scenes in a file for 5W1H extraction. Uses the pipeline's `parent_chunk_5w1h.py --mode llm`. + +### Request + +```json +{ + "file_uuid": "3abeee81d94597629ed8cb943f182e94" +} +``` + +## GET /api/v1/agents/5w1h/status + +Get status of the 5W1H agent pipeline for a file. + +--- + +## Embedding Model + +| Detail | Value | +|--------|-------| +| **Model** | EmbeddingGemma-300m | +| **Endpoint** | `POST /v1/embeddings` on port 11436 | +| **Dimension** | 768 | +| **Used by** | `parent_chunk_5w1h.py --embed`, story, 5W1H, search | + diff --git a/docs_v1.0/doc_wasm/pkg/doc_wasm.js b/docs_v1.0/doc_wasm/pkg/doc_wasm.js new file mode 100644 index 0000000..121d078 --- /dev/null +++ b/docs_v1.0/doc_wasm/pkg/doc_wasm.js @@ -0,0 +1,232 @@ +/* @ts-self-types="./doc_wasm.d.ts" */ + +/** + * @returns {string} + */ +export function module_list() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.module_list(retptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred1_0, deferred1_1, 1); + } +} + +/** + * @param {string} md + * @returns {string} + */ +export function render_markdown(md) { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(md, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const len0 = WASM_VECTOR_LEN; + wasm.render_markdown(retptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred2_0 = r0; + deferred2_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export(deferred2_0, deferred2_1, 1); + } +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + }; + return { + __proto__: null, + "./doc_wasm_bg.js": import0, + }; +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('doc_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm b/docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a5c27fb685a9233fab800bd1d75b1e63aa0484ce GIT binary patch literal 228029 zcmdqK4V;}^t7P$M(}uz==U(_+KIil! zRm&A;5J<_bJ=aR7P$5Fpa2hpAg@~aBC@|H4K?{r+G+?0tdo*g5ffF!zdNfB01T6G^ zfB&_fXFoHUmKQn4b8pk+SJXxoi`_U zlHwnqj9+tedZ%*qFIo0@&CM0w$g~H!1A67%Kgp4IQ z>Sof3<7TTJSDRG|HeLHR4aT=<3v61A3aevP8efa1!P(j_|0ks z&}(U}R;yHM^jB>(;z}Gh8Zobbw&*EI)q{%jUqX)z zkpJUmEp1fOv>yJe3^H=E(;8*e7r<3x3e*zDItYrW4uokfs>QWL-zk((hX>Ud0gyH- zjhGSh8du=BxH1sGD!%Eafi#X*RU2@fo1LAHnzj9Lt@_h1{pnZU`X7_1`O~kw<^O*9 zOJDNxpL+G4=tIehTVMK$TV8tWOMd#tZk@U1l|S=}=(F*Lm%QX>e*C9iam!D>^c64p zv0H9=$(~of?dYk@~T(<)GPM9^w#J=y0TQ{rMJK8mACGRo=8@mm-(#dcb3V$ zJo??FaWsx27dQXIucWU_e?9qT{O06W(|0DX`eO3(Ur4`{{95|Q$+zNnCvQp5#joC% zek{2^eRuli_(1%gWX)UCuf=yIbLo52-%s9_{%P`_^jPvI$-~LVlM~6;l3z~tCLc%+ zrT>t8BKczccgg3I&nBmn&n5pQSxCN+{8{qflTRk^OOB?WPQIM{OT6co)4k~f$s5x9 z(#MiNNZuNMCVqeVt>l}@SCZM}wl}9o(l@5pek6Wp`r7n|4<`4dgRg&U_~QG@nmu!?xpR>Mcqm(Zf3E2B%1CH@O+GCm1ljrTjOz#*E-LG z@pQM5C2l^Q)~7*|3yCG0B==S<*$M^LPj}lSKb=l@hj`w_a|KUTSTo)2gi`UX-IXL& zm*gsv_tQAZ)uF(^baz+`O?FWR>ZyAUo3tXinol9`{CFa$7d)?yl_S?{&L7H+4rnZtjnJW7+8Q+fl}2raP)$ zvr6Yh6uxu1H`S+p>EE%oW>NKCb4&$PRPs^nR{(a9w0jq39u z`nu7VyjLX$_DpY%BGXW)tdJZywmnPl-DhPa94QN1X>^V5Z0u;84$V>T+CdRkHL3 zcqHzQw$f(CTb8;xK`;XV{`F9Jg*c;zloYh||74>>3BnGzncXO6(j!vfCu`KR93<>-nu>|Zl-e;cioOvv7T#Pdw7-!TakT)(&)W4skE9ok9FX&D(xWWofm8Py)l>U zB+KN$3(5LjZQf91bhTbxt?zeL7dIKvTJ&V2IT6tELBTh?JvyfM^;?sNJbI1=^az~w zupi(A2n{Ng+!u=g=pa2o@PDKL4c(rNb}z`rx*J+mDGm?xhhlXf+E20z&}-@sEcW|v zHJWaqJ0{|9jy{IAmaK>RuhgKsV0;(JKF=_4u Q7mcJ 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�i2)p=Ufk%s1&#l?mebjAzWVhCc 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 literal 0 HcmV?d00001 diff --git a/src/api/server.rs b/src/api/server.rs index e414487..49399e9 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -3447,6 +3447,40 @@ async fn unregister( } /// Serve documentation HTML pages with cookie-based auth. +async fn wasm_doc_handler() -> Result { + let path = std::path::Path::new("/Users/accusys/momentry_core_0.1/docs_v1.0/doc_wasm/index.html"); + match tokio::fs::read_to_string(path).await { + Ok(html) => Ok(([("content-type", "text/html; charset=utf-8")], html)), + Err(_) => Err((StatusCode::NOT_FOUND, "Doc not found")), + } +} + +async fn wasm_doc_file_handler( + Path(file): Path, +) -> Result { + if file.contains("..") || file.contains("//") { + return Err((StatusCode::NOT_FOUND, "Invalid path")); + } + let base = std::path::Path::new("/Users/accusys/momentry_core_0.1/docs_v1.0/doc_wasm"); + let path = base.join(&file); + if !path.exists() || !path.starts_with(base) { + return Err((StatusCode::NOT_FOUND, "File not found")); + } + let data = tokio::fs::read(&path).await.map_err(|_| (StatusCode::NOT_FOUND, "Read error"))?; + let mime = if file.ends_with(".wasm") { + "application/wasm" + } else if file.ends_with(".js") { + "application/javascript" + } else if file.ends_with(".md") { + "text/markdown; charset=utf-8" + } else if file.ends_with(".css") { + "text/css" + } else { + "application/octet-stream" + }; + Ok(([("content-type", mime)], data)) +} + async fn doc_handler( State(state): State, headers: axum::http::HeaderMap, @@ -3673,6 +3707,8 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> { .route("/doc", get(doc_handler)) .route("/doc/*file", get(doc_file_handler)) .route("/dev-doc", get(dev_doc_handler)) + .route("/doc-wasm", get(wasm_doc_handler)) + .route("/doc-wasm/*file", get(wasm_doc_file_handler)) .route("/api/v1/auth/login", post(login)) .route("/api/v1/auth/logout", post(logout)) .route("/api/v1/stats/sftpgo", get(get_sftpgo_status)) @@ -3813,16 +3849,20 @@ async fn get_ingestion_status( let tkg_edges = count_sql!(&format!("SELECT COUNT(*) FROM {} WHERE file_uuid = '{file_uuid}'", schema::table_name("tkg_edges"))); let scene_5w1h = count_sql!(&format!("SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'cut' AND summary_text IS NOT NULL AND summary_text != ''")); - let related_identities: Vec = sqlx::query_as::<_, (String, String)>(&format!( - "SELECT DISTINCT i.uuid, i.name FROM {identities} i \ + let related_identities: Vec = match sqlx::query_as::<_, (String, String)>(&format!( + "SELECT DISTINCT i.uuid::text, i.name FROM {identities} i \ JOIN {fd} fd ON fd.identity_id = i.id \ WHERE fd.file_uuid = '{file_uuid}' AND fd.identity_id IS NOT NULL \ ORDER BY i.name" - )).fetch_all(pool).await.unwrap_or_default().into_iter() - .map(|(uuid, name)| { - let uuid = uuid.replace('-', ""); - IdentityRef { uuid, name } - }).collect(); + )).fetch_all(pool).await { + Ok(rows) => rows.into_iter().map(|(uuid, name)| { + IdentityRef { uuid: uuid.replace('-', ""), name } + }).collect(), + Err(e) => { + tracing::error!("related_identities query failed: {}", e); + vec![] + } + }; let strangers = count_sql!(&format!( "SELECT COUNT(DISTINCT trace_id) FROM {fd} \