feat: WASM-based doc viewer (pulldown-cmark)
This commit is contained in:
224
doc_wasm/Cargo.lock
generated
Normal file
224
doc_wasm/Cargo.lock
generated
Normal file
@@ -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"
|
||||||
18
doc_wasm/Cargo.toml
Normal file
18
doc_wasm/Cargo.toml
Normal file
@@ -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
|
||||||
29
doc_wasm/src/lib.rs
Normal file
29
doc_wasm/src/lib.rs
Normal file
@@ -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("<table>", "<table class=\"table\">");
|
||||||
|
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()
|
||||||
|
}
|
||||||
15
docs_v1.0/API_WORKSPACE/Makefile
Normal file
15
docs_v1.0/API_WORKSPACE/Makefile
Normal file
@@ -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)"
|
||||||
127
docs_v1.0/doc_wasm/index.html
Normal file
127
docs_v1.0/doc_wasm/index.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Momentry API Docs (WASM)</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; }
|
||||||
|
#app { display: flex; min-height: 100vh; }
|
||||||
|
.sidebar { width: 260px; background: #fff; border-right: 1px solid #ddd; padding: 20px; display: flex; flex-direction: column; }
|
||||||
|
.sidebar h1 { font-size: 18px; margin-bottom: 16px; }
|
||||||
|
.sidebar a { display: block; padding: 6px 0; color: #0066cc; text-decoration: none; font-size: 14px; cursor: pointer; }
|
||||||
|
.sidebar a:hover { color: #003d80; }
|
||||||
|
.sidebar .active { font-weight: 600; color: #003d80; }
|
||||||
|
.sidebar .lang-toggle { margin: 12px 0; font-size: 12px; }
|
||||||
|
.sidebar .lang-toggle a { display: inline; font-size: 12px; padding: 2px 6px; }
|
||||||
|
.sidebar .logout { margin-top: auto; padding-top: 16px; border-top: 1px solid #eee; }
|
||||||
|
.sidebar .logout a { font-size: 12px; color: #999; }
|
||||||
|
.sidebar .logout a:hover { color: #cc0000; }
|
||||||
|
.content { flex: 1; padding: 40px; max-width: 960px; }
|
||||||
|
.content.loading { opacity: 0.5; }
|
||||||
|
.content table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||||
|
.content th, .content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||||
|
.content th { background: #f0f0f0; font-weight: 600; }
|
||||||
|
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||||
|
.content pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||||
|
.content pre code { background: none; padding: 0; }
|
||||||
|
.content h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||||
|
.content h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||||
|
.content h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||||
|
.content p { line-height: 1.6; margin: 8px 0; }
|
||||||
|
.content a { color: #0066cc; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="sidebar">
|
||||||
|
<h1>Momentry Docs</h1>
|
||||||
|
<div id="module-list"></div>
|
||||||
|
<div class="logout">
|
||||||
|
<a href="#" id="logout-btn">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content" id="content">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, { render_markdown, module_list } from './pkg/doc_wasm.js';
|
||||||
|
|
||||||
|
const API = '';
|
||||||
|
let currentModule = null;
|
||||||
|
let modules = [];
|
||||||
|
|
||||||
|
async function loadDoc(name) {
|
||||||
|
const el = document.getElementById('content');
|
||||||
|
el.classList.add('loading');
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/doc/modules/${name}.md`);
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const md = await resp.text();
|
||||||
|
el.innerHTML = render_markdown(md);
|
||||||
|
el.classList.remove('loading');
|
||||||
|
// highlight active module
|
||||||
|
document.querySelectorAll('.sidebar a.module-link').forEach(a => a.classList.remove('active'));
|
||||||
|
const link = document.querySelector(`.sidebar a[data-module="${name}"]`);
|
||||||
|
if (link) link.classList.add('active');
|
||||||
|
history.pushState(null, '', `#${name}`);
|
||||||
|
} catch(e) {
|
||||||
|
el.innerHTML = `<p style="color:red">Failed to load: ${e.message}</p>`;
|
||||||
|
el.classList.remove('loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAuth() {
|
||||||
|
const resp = await fetch(`${API}/api/v1/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username:'demo',password:'demo'})
|
||||||
|
});
|
||||||
|
return resp.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initApp() {
|
||||||
|
await init();
|
||||||
|
|
||||||
|
// Parse module list
|
||||||
|
modules = JSON.parse(module_list());
|
||||||
|
const listEl = document.getElementById('module-list');
|
||||||
|
modules.forEach(([name, cn, en]) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.className = 'module-link';
|
||||||
|
a.dataset.module = name;
|
||||||
|
a.textContent = `${cn} - ${en}`;
|
||||||
|
a.onclick = (e) => { e.preventDefault(); loadDoc(name); };
|
||||||
|
listEl.appendChild(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
document.getElementById('logout-btn').onclick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await fetch(`${API}/api/v1/auth/logout`, {method:'POST'});
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load initial module from hash or default
|
||||||
|
const hash = location.hash.slice(1);
|
||||||
|
if (hash) {
|
||||||
|
loadDoc(hash);
|
||||||
|
} else {
|
||||||
|
loadDoc('01_auth');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle browser back/forward
|
||||||
|
window.onpopstate = () => {
|
||||||
|
const hash = location.hash.slice(1);
|
||||||
|
if (hash) loadDoc(hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
initApp();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
280
docs_v1.0/doc_wasm/modules/01_auth.md
Normal file
280
docs_v1.0/doc_wasm/modules/01_auth.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<!-- module: auth -->
|
||||||
|
<!-- description: Authentication — login, logout, JWT, session cookie, API key -->
|
||||||
|
<!-- depends: -->
|
||||||
|
|
||||||
|
## 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=<session_id>` | 24h | per-browser session | Portal (browser) |
|
||||||
|
| **JWT** | `Authorization: Bearer <token>` | 1h | per-login token | API clients, CLI, scripts |
|
||||||
|
| **API Key** | `X-API-Key: <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 <jwt>`. Expires in 1 hour. |
|
||||||
|
| `api_key` | string | Legacy API key. Use as `X-API-Key: <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=<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=<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=<uuid>"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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)
|
||||||
147
docs_v1.0/doc_wasm/modules/02_health.md
Normal file
147
docs_v1.0/doc_wasm/modules/02_health.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<!-- module: health -->
|
||||||
|
<!-- description: Health check endpoints -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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 |
|
||||||
184
docs_v1.0/doc_wasm/modules/03_register.md
Normal file
184
docs_v1.0/doc_wasm/modules/03_register.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<!-- module: register -->
|
||||||
|
<!-- description: File registration — register, scan -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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. |
|
||||||
138
docs_v1.0/doc_wasm/modules/04_lookup.md
Normal file
138
docs_v1.0/doc_wasm/modules/04_lookup.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<!-- module: lookup -->
|
||||||
|
<!-- description: File lookup by name and unregistration -->
|
||||||
|
<!-- depends: 01_auth, 03_register -->
|
||||||
|
|
||||||
|
## 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 |
|
||||||
236
docs_v1.0/doc_wasm/modules/05_process.md
Normal file
236
docs_v1.0/doc_wasm/modules/05_process.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!-- module: process -->
|
||||||
|
<!-- description: Processing pipeline — trigger, probe, progress, jobs -->
|
||||||
|
<!-- depends: 01_auth, 03_register -->
|
||||||
|
|
||||||
|
## 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 |
|
||||||
145
docs_v1.0/doc_wasm/modules/06_search.md
Normal file
145
docs_v1.0/doc_wasm/modules/06_search.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<!-- module: search -->
|
||||||
|
<!-- description: Vector search, BM25, smart search, universal search, visual search -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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) |
|
||||||
333
docs_v1.0/doc_wasm/modules/07_identity.md
Normal file
333
docs_v1.0/doc_wasm/modules/07_identity.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
<!-- module: identity -->
|
||||||
|
<!-- description: Global identities — CRUD, detail, files, faces, bind, unbind, search -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
65
docs_v1.0/doc_wasm/modules/08_identity_agent.md
Normal file
65
docs_v1.0/doc_wasm/modules/08_identity_agent.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<!-- module: identity_agent -->
|
||||||
|
<!-- description: Identity agent — match from photo, match from trace -->
|
||||||
|
<!-- depends: 01_auth, 07_identity -->
|
||||||
|
|
||||||
|
## 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}'
|
||||||
|
```
|
||||||
146
docs_v1.0/doc_wasm/modules/08_media.md
Normal file
146
docs_v1.0/doc_wasm/modules/08_media.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!-- module: media -->
|
||||||
|
<!-- description: Video streaming & frame extraction -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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) |
|
||||||
109
docs_v1.0/doc_wasm/modules/09_tmdb.md
Normal file
109
docs_v1.0/doc_wasm/modules/09_tmdb.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<!-- module: tmdb -->
|
||||||
|
<!-- description: TMDb enrichment endpoints — prefetch, probe, resource, check -->
|
||||||
|
<!-- depends: 01_auth, 03_register -->
|
||||||
|
|
||||||
|
## 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
|
||||||
|
}
|
||||||
|
```
|
||||||
178
docs_v1.0/doc_wasm/modules/10_pipeline.md
Normal file
178
docs_v1.0/doc_wasm/modules/10_pipeline.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<!-- module: pipeline -->
|
||||||
|
<!-- description: Pipeline processors, ingestion status, stats endpoints -->
|
||||||
|
<!-- depends: 01_auth -->
|
||||||
|
|
||||||
|
## 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` |
|
||||||
57
docs_v1.0/doc_wasm/modules/11_error_codes.md
Normal file
57
docs_v1.0/doc_wasm/modules/11_error_codes.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!-- module: error_codes -->
|
||||||
|
<!-- description: Standard API error codes -->
|
||||||
|
<!-- depends: -->
|
||||||
|
|
||||||
|
## 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 |
|
||||||
118
docs_v1.0/doc_wasm/modules/12_agent.md
Normal file
118
docs_v1.0/doc_wasm/modules/12_agent.md
Normal file
@@ -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 |
|
||||||
|
|
||||||
232
docs_v1.0/doc_wasm/pkg/doc_wasm.js
Normal file
232
docs_v1.0/doc_wasm/pkg/doc_wasm.js
Normal file
@@ -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 };
|
||||||
BIN
docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm
Normal file
BIN
docs_v1.0/doc_wasm/pkg/doc_wasm_bg.wasm
Normal file
Binary file not shown.
@@ -3447,6 +3447,40 @@ async fn unregister(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Serve documentation HTML pages with cookie-based auth.
|
/// Serve documentation HTML pages with cookie-based auth.
|
||||||
|
async fn wasm_doc_handler() -> Result<impl axum::response::IntoResponse, (StatusCode, &'static str)> {
|
||||||
|
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<String>,
|
||||||
|
) -> Result<impl axum::response::IntoResponse, (StatusCode, &'static str)> {
|
||||||
|
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(
|
async fn doc_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: axum::http::HeaderMap,
|
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", get(doc_handler))
|
||||||
.route("/doc/*file", get(doc_file_handler))
|
.route("/doc/*file", get(doc_file_handler))
|
||||||
.route("/dev-doc", get(dev_doc_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/login", post(login))
|
||||||
.route("/api/v1/auth/logout", post(logout))
|
.route("/api/v1/auth/logout", post(logout))
|
||||||
.route("/api/v1/stats/sftpgo", get(get_sftpgo_status))
|
.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 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 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<IdentityRef> = sqlx::query_as::<_, (String, String)>(&format!(
|
let related_identities: Vec<IdentityRef> = match sqlx::query_as::<_, (String, String)>(&format!(
|
||||||
"SELECT DISTINCT i.uuid, i.name FROM {identities} i \
|
"SELECT DISTINCT i.uuid::text, i.name FROM {identities} i \
|
||||||
JOIN {fd} fd ON fd.identity_id = i.id \
|
JOIN {fd} fd ON fd.identity_id = i.id \
|
||||||
WHERE fd.file_uuid = '{file_uuid}' AND fd.identity_id IS NOT NULL \
|
WHERE fd.file_uuid = '{file_uuid}' AND fd.identity_id IS NOT NULL \
|
||||||
ORDER BY i.name"
|
ORDER BY i.name"
|
||||||
)).fetch_all(pool).await.unwrap_or_default().into_iter()
|
)).fetch_all(pool).await {
|
||||||
.map(|(uuid, name)| {
|
Ok(rows) => rows.into_iter().map(|(uuid, name)| {
|
||||||
let uuid = uuid.replace('-', "");
|
IdentityRef { uuid: uuid.replace('-', ""), name }
|
||||||
IdentityRef { uuid, name }
|
}).collect(),
|
||||||
}).collect();
|
Err(e) => {
|
||||||
|
tracing::error!("related_identities query failed: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let strangers = count_sql!(&format!(
|
let strangers = count_sql!(&format!(
|
||||||
"SELECT COUNT(DISTINCT trace_id) FROM {fd} \
|
"SELECT COUNT(DISTINCT trace_id) FROM {fd} \
|
||||||
|
|||||||
Reference in New Issue
Block a user