refactor: modularize server.rs into separate route modules
- Extract scan.rs, files.rs, types.rs, processing.rs, visual_chunk_search.rs - Move AppState and AppConfig to types.rs - Each module exposes pub fn xxx_routes() -> Router<AppState> - server.rs reduced from 5005 to 118 lines (orchestrator only) - All stubs filled with real implementations from git history - Verify: cargo check, clippy, tests all pass
This commit is contained in:
@@ -2,7 +2,7 @@ use axum::{extract::State, http::StatusCode, response::Json, routing::post, Rout
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::api::server::AppState;
|
use crate::api::types::AppState;
|
||||||
|
|
||||||
pub fn agent_routes() -> Router<AppState> {
|
pub fn agent_routes() -> Router<AppState> {
|
||||||
Router::new().route("/api/v1/agents/translate", post(translate_text))
|
Router::new().route("/api/v1/agents/translate", post(translate_text))
|
||||||
|
|||||||
139
src/api/auth.rs
Normal file
139
src/api/auth.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::middleware::extract_cookies;
|
||||||
|
use super::types::AppState;
|
||||||
|
|
||||||
|
static DEMO_USER_API_KEY: Lazy<String> = Lazy::new(|| {
|
||||||
|
std::env::var("MOMENTRY_DEMO_API_KEY")
|
||||||
|
.unwrap_or_else(|_| "muser_demo_key_32chars_abcdef1234567890".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LoginRequest {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct LoginResponse {
|
||||||
|
success: bool,
|
||||||
|
message: Option<String>,
|
||||||
|
api_key: Option<String>,
|
||||||
|
user: Option<UserInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct UserInfo {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<LoginRequest>,
|
||||||
|
) -> Result<axum::response::Response<axum::body::Body>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let (user_id, username, role) = 'resolve: {
|
||||||
|
if let Ok(Some((uid, uname, pw_hash, role_str))) =
|
||||||
|
state.db.get_user_by_username(&req.username).await
|
||||||
|
{
|
||||||
|
if crate::core::auth::password::verify_password(&req.password, &pw_hash) {
|
||||||
|
break 'resolve (uid, uname, role_str);
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
"[LOGIN] Local password mismatch for {}, trying SFTPGo",
|
||||||
|
&req.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.username == "demo" && req.password == "demo" {
|
||||||
|
let uid = state
|
||||||
|
.db
|
||||||
|
.get_user_by_username("demo")
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|(id, _, _, _)| id)
|
||||||
|
.unwrap_or(0);
|
||||||
|
break 'resolve (uid, "demo".to_string(), "user".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"success": false, "message": "Invalid username or password"
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let jwt_token = crate::core::auth::jwt::create_jwt(user_id, &username, &role).map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"success": false, "message": format!("JWT creation failed: {}", e)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let session_id = uuid::Uuid::new_v4().to_string().replace('-', "");
|
||||||
|
state
|
||||||
|
.db
|
||||||
|
.create_session(&session_id, user_id, &DEMO_USER_API_KEY, 24)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if user_id > 0 {
|
||||||
|
state.db.update_last_login(user_id).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"jwt": jwt_token,
|
||||||
|
"api_key": DEMO_USER_API_KEY.clone(),
|
||||||
|
"user": {
|
||||||
|
"username": username,
|
||||||
|
"role": role
|
||||||
|
},
|
||||||
|
"expires_at": (chrono::Utc::now() + chrono::Duration::hours(24)).to_rfc3339()
|
||||||
|
});
|
||||||
|
|
||||||
|
let json_body = axum::body::Body::from(serde_json::to_string(&body).unwrap_or_default());
|
||||||
|
let response = axum::response::Response::builder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header(
|
||||||
|
"Set-Cookie",
|
||||||
|
format!(
|
||||||
|
"session_id={}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400",
|
||||||
|
session_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.body(json_body)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<serde_json::value::Value> {
|
||||||
|
let cookies = extract_cookies(&headers);
|
||||||
|
if let Some(sid) = cookies
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k == "session_id")
|
||||||
|
.map(|(_, v)| v.clone())
|
||||||
|
{
|
||||||
|
state.db.delete_session(&sid).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Logged out"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/v1/auth/login", post(login))
|
||||||
|
.route("/api/v1/auth/logout", post(logout))
|
||||||
|
}
|
||||||
54
src/api/docs.rs
Normal file
54
src/api/docs.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use axum::{extract::Path, http::StatusCode, routing::get, Router};
|
||||||
|
|
||||||
|
use super::types::AppState;
|
||||||
|
|
||||||
|
async fn doc_redirect() -> axum::response::Redirect {
|
||||||
|
axum::response::Redirect::to("/doc-wasm")
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn doc_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/doc", get(doc_redirect))
|
||||||
|
.route("/doc/*file", get(doc_redirect))
|
||||||
|
.route("/dev-doc", get(doc_redirect))
|
||||||
|
.route("/doc-wasm", get(wasm_doc_handler))
|
||||||
|
.route("/doc-wasm/*file", get(wasm_doc_file_handler))
|
||||||
|
}
|
||||||
1105
src/api/files.rs
Normal file
1105
src/api/files.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ use reqwest::Client;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
|
|
||||||
use crate::api::server::AppState;
|
use crate::api::types::AppState;
|
||||||
use crate::core::db::qdrant_db::QdrantDb;
|
use crate::core::db::qdrant_db::QdrantDb;
|
||||||
use crate::core::db::schema;
|
use crate::core::db::schema;
|
||||||
use crate::core::db::{PostgresDb, VectorPayload};
|
use crate::core::db::{PostgresDb, VectorPayload};
|
||||||
|
|||||||
680
src/api/health.rs
Normal file
680
src/api/health.rs
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, response::Json, routing::get, Router};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use super::types::AppState;
|
||||||
|
use crate::core::cache::MongoCache;
|
||||||
|
use crate::core::config;
|
||||||
|
use crate::core::db::{Database, PostgresDb, RedisClient};
|
||||||
|
use crate::worker::resources::SystemResources;
|
||||||
|
|
||||||
|
// Global State
|
||||||
|
static SERVER_START: OnceCell<Instant> = OnceCell::new();
|
||||||
|
static SERVER_HOST: OnceCell<String> = OnceCell::new();
|
||||||
|
static SERVER_PORT: OnceCell<u16> = OnceCell::new();
|
||||||
|
|
||||||
|
pub fn init_server_state(host: &str, port: u16) {
|
||||||
|
let _ = SERVER_START.set(Instant::now());
|
||||||
|
let resolved_ip = if host == "0.0.0.0" {
|
||||||
|
if let Ok(addrs) = std::net::ToSocketAddrs::to_socket_addrs(&"localhost:0") {
|
||||||
|
addrs
|
||||||
|
.filter_map(|a| if a.is_ipv4() { Some(a.ip()) } else { None })
|
||||||
|
.next()
|
||||||
|
.map(|ip| ip.to_string())
|
||||||
|
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||||
|
} else {
|
||||||
|
"127.0.0.1".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
host.to_string()
|
||||||
|
};
|
||||||
|
let _ = SERVER_HOST.set(resolved_ip);
|
||||||
|
let _ = SERVER_PORT.set(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_host() -> String {
|
||||||
|
SERVER_HOST
|
||||||
|
.get()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0.0.0.0".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_port() -> u16 {
|
||||||
|
SERVER_PORT.get().copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_uptime_ms() -> u64 {
|
||||||
|
SERVER_START
|
||||||
|
.get()
|
||||||
|
.map(|i| i.elapsed().as_millis() as u64)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct HealthResponse {
|
||||||
|
ip: String,
|
||||||
|
port: u16,
|
||||||
|
status: String,
|
||||||
|
version: String,
|
||||||
|
build_git_hash: String,
|
||||||
|
build_timestamp: String,
|
||||||
|
uptime_ms: u64,
|
||||||
|
watcher_running: bool,
|
||||||
|
worker_running: bool,
|
||||||
|
auto_pipeline_enabled: bool,
|
||||||
|
watcher_auto_register_enabled: bool,
|
||||||
|
system_timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct DetailedHealthResponse {
|
||||||
|
ip: String,
|
||||||
|
port: u16,
|
||||||
|
status: String,
|
||||||
|
version: String,
|
||||||
|
build_git_hash: String,
|
||||||
|
build_timestamp: String,
|
||||||
|
uptime_ms: u64,
|
||||||
|
services: ServiceHealth,
|
||||||
|
resources: ResourceStatus,
|
||||||
|
pipeline: PipelineStatus,
|
||||||
|
schema: SchemaHealth,
|
||||||
|
identities: IdentityHealth,
|
||||||
|
integrations: IntegrationHealth,
|
||||||
|
config: ConfigHealth,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct IntegrationHealth {
|
||||||
|
tmdb: crate::core::tmdb::status::TmdbResourceStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct IdentityHealth {
|
||||||
|
directory_exists: bool,
|
||||||
|
files_count: usize,
|
||||||
|
index_ok: bool,
|
||||||
|
db_count: i64,
|
||||||
|
synced: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ConfigHealth {
|
||||||
|
cache_enabled: bool,
|
||||||
|
auto_pipeline_enabled: bool,
|
||||||
|
watcher_auto_register_enabled: bool,
|
||||||
|
system_timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct SchemaHealth {
|
||||||
|
pub table_exists: bool,
|
||||||
|
pub applied: Vec<MigrationInfo>,
|
||||||
|
pub required: Vec<MigrationInfo>,
|
||||||
|
pub ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct MigrationInfo {
|
||||||
|
pub filename: String,
|
||||||
|
pub checksum: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PipelineStatus {
|
||||||
|
scripts_ready: bool,
|
||||||
|
scripts_count: usize,
|
||||||
|
processors: ProcessorInventory,
|
||||||
|
models_ready: bool,
|
||||||
|
models_count: usize,
|
||||||
|
scripts_integrity: ScriptIntegrity,
|
||||||
|
ffmpeg: bool,
|
||||||
|
embedding_server: ServiceStatus,
|
||||||
|
gdino_api: ServiceStatus,
|
||||||
|
llm: ServiceStatus,
|
||||||
|
rsync: ServiceStatus,
|
||||||
|
watcher_running: bool,
|
||||||
|
worker_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ScriptIntegrity {
|
||||||
|
matched: usize,
|
||||||
|
total: usize,
|
||||||
|
ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ProcessorInventory {
|
||||||
|
asr: bool,
|
||||||
|
yolo: bool,
|
||||||
|
face: bool,
|
||||||
|
pose: bool,
|
||||||
|
ocr: bool,
|
||||||
|
cut: bool,
|
||||||
|
caption: bool,
|
||||||
|
scene: bool,
|
||||||
|
story: bool,
|
||||||
|
asrx: bool,
|
||||||
|
probe: bool,
|
||||||
|
visual_chunk: bool,
|
||||||
|
total_py_files: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ResourceStatus {
|
||||||
|
cpu_used_percent: f64,
|
||||||
|
cpu_idle_percent: f64,
|
||||||
|
memory_available_mb: u64,
|
||||||
|
memory_total_mb: u64,
|
||||||
|
memory_used_percent: f64,
|
||||||
|
gpu_available: bool,
|
||||||
|
gpu_utilization: Option<f64>,
|
||||||
|
gpu_memory_used_pct: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ServiceHealth {
|
||||||
|
postgres: ServiceStatus,
|
||||||
|
redis: ServiceStatus,
|
||||||
|
qdrant: ServiceStatus,
|
||||||
|
mongodb: ServiceStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ServiceStatus {
|
||||||
|
status: String,
|
||||||
|
latency_ms: Option<u64>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||||
|
let postgres = check_postgres().await;
|
||||||
|
let redis = check_redis().await;
|
||||||
|
let qdrant = check_qdrant().await;
|
||||||
|
let mongodb = check_mongodb(&state.mongo_cache).await;
|
||||||
|
|
||||||
|
let all_ok = postgres.status == "ok"
|
||||||
|
&& redis.status == "ok"
|
||||||
|
&& qdrant.status == "ok"
|
||||||
|
&& mongodb.status == "ok";
|
||||||
|
|
||||||
|
let status = if all_ok { "ok" } else { "degraded" };
|
||||||
|
|
||||||
|
if all_ok {
|
||||||
|
let _ = state.redis_cache.set_health(status).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(HealthResponse {
|
||||||
|
ip: get_host(),
|
||||||
|
port: get_port(),
|
||||||
|
status: status.to_string(),
|
||||||
|
version: env!("BUILD_VERSION").to_string(),
|
||||||
|
build_git_hash: env!("BUILD_GIT_HASH").to_string(),
|
||||||
|
build_timestamp: env!("BUILD_TIMESTAMP").to_string(),
|
||||||
|
uptime_ms: get_uptime_ms(),
|
||||||
|
watcher_running: check_process_running("watcher"),
|
||||||
|
worker_running: check_process_running("worker"),
|
||||||
|
auto_pipeline_enabled: config::get_auto_pipeline_enabled(),
|
||||||
|
watcher_auto_register_enabled: config::get_watcher_auto_register(),
|
||||||
|
system_timezone: config::SYSTEM_TIMEZONE.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_detailed(State(state): State<AppState>) -> Json<DetailedHealthResponse> {
|
||||||
|
let postgres = check_postgres().await;
|
||||||
|
let redis = check_redis().await;
|
||||||
|
let qdrant = check_qdrant().await;
|
||||||
|
let mongodb = check_mongodb(&state.mongo_cache).await;
|
||||||
|
|
||||||
|
let overall_status = if postgres.status == "ok"
|
||||||
|
&& redis.status == "ok"
|
||||||
|
&& qdrant.status == "ok"
|
||||||
|
&& mongodb.status == "ok"
|
||||||
|
{
|
||||||
|
"ok"
|
||||||
|
} else {
|
||||||
|
"degraded"
|
||||||
|
};
|
||||||
|
|
||||||
|
let sys = SystemResources::check();
|
||||||
|
|
||||||
|
let scripts_base = config::SCRIPTS_DIR.clone();
|
||||||
|
let scripts_dir = std::path::Path::new(&scripts_base);
|
||||||
|
let scripts_path = scripts_dir.to_path_buf();
|
||||||
|
let models_path = std::path::PathBuf::from("/Users/accusys/momentry_core_0.1/models");
|
||||||
|
|
||||||
|
let py_files = std::fs::read_dir(&scripts_path)
|
||||||
|
.map(|d| {
|
||||||
|
d.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| e.path().extension().map(|x| x == "py").unwrap_or(false))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let total_model_files = std::fs::read_dir(&models_path)
|
||||||
|
.map(|d| {
|
||||||
|
d.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| {
|
||||||
|
let p = e.path();
|
||||||
|
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or("");
|
||||||
|
matches!(ext, "pt" | "mlpackage" | "gguf" | "bin" | "onnx")
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let check_script = |name: &str| -> bool {
|
||||||
|
let candidate = scripts_path.join(name);
|
||||||
|
candidate.exists()
|
||||||
|
};
|
||||||
|
|
||||||
|
let check_python_module = |module: &str| -> bool {
|
||||||
|
std::process::Command::new(&*config::PYTHON_PATH)
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!("import {}", module))
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let checksums_path = scripts_path.join("checksums.sha256");
|
||||||
|
let scripts_integrity = match std::fs::read_to_string(&checksums_path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let mut matched = 0usize;
|
||||||
|
let mut total = 0usize;
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = line.splitn(2, ' ').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let expected_hash = parts[0];
|
||||||
|
let file_path = parts[1].trim_start();
|
||||||
|
total += 1;
|
||||||
|
let full_path = scripts_path.join(file_path);
|
||||||
|
if full_path.exists() {
|
||||||
|
if let Ok(actual) = std::process::Command::new("shasum")
|
||||||
|
.arg("-a")
|
||||||
|
.arg("256")
|
||||||
|
.arg(&full_path)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
let out = String::from_utf8_lossy(&actual.stdout);
|
||||||
|
let actual_hash = out.split(' ').next().unwrap_or("").to_string();
|
||||||
|
if actual_hash == expected_hash {
|
||||||
|
matched += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScriptIntegrity {
|
||||||
|
matched,
|
||||||
|
total,
|
||||||
|
ok: matched == total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => ScriptIntegrity {
|
||||||
|
matched: 0,
|
||||||
|
total: 0,
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(DetailedHealthResponse {
|
||||||
|
ip: get_host(),
|
||||||
|
port: get_port(),
|
||||||
|
status: overall_status.to_string(),
|
||||||
|
version: env!("BUILD_VERSION").to_string(),
|
||||||
|
build_git_hash: env!("BUILD_GIT_HASH").to_string(),
|
||||||
|
build_timestamp: env!("BUILD_TIMESTAMP").to_string(),
|
||||||
|
uptime_ms: get_uptime_ms(),
|
||||||
|
services: ServiceHealth {
|
||||||
|
postgres,
|
||||||
|
redis,
|
||||||
|
qdrant,
|
||||||
|
mongodb,
|
||||||
|
},
|
||||||
|
resources: ResourceStatus {
|
||||||
|
cpu_used_percent: sys.cpu_used_percent,
|
||||||
|
cpu_idle_percent: sys.cpu_idle_percent,
|
||||||
|
memory_available_mb: sys.memory_available_mb,
|
||||||
|
memory_total_mb: sys.memory_total_mb,
|
||||||
|
memory_used_percent: sys.memory_used_percent,
|
||||||
|
gpu_available: sys.gpu_available,
|
||||||
|
gpu_utilization: sys.gpu_utilization,
|
||||||
|
gpu_memory_used_pct: sys.gpu_memory_used_pct,
|
||||||
|
},
|
||||||
|
pipeline: PipelineStatus {
|
||||||
|
scripts_ready: scripts_path.is_dir(),
|
||||||
|
scripts_count: py_files,
|
||||||
|
scripts_integrity,
|
||||||
|
processors: ProcessorInventory {
|
||||||
|
asr: check_script("asr_processor.py"),
|
||||||
|
yolo: check_script("yolo_processor.py"),
|
||||||
|
face: check_script("face_processor.py"),
|
||||||
|
pose: check_script("pose_processor.py"),
|
||||||
|
ocr: check_script("ocr_processor.py"),
|
||||||
|
cut: check_script("cut_processor.py"),
|
||||||
|
caption: check_script("caption_processor.py"),
|
||||||
|
scene: check_script("scene_classifier.py"),
|
||||||
|
story: check_script("story_processor.py"),
|
||||||
|
asrx: check_script("asrx_processor.py"),
|
||||||
|
probe: check_script("probe_file.py"),
|
||||||
|
visual_chunk: check_script("visual_chunk_processor.py"),
|
||||||
|
total_py_files: py_files,
|
||||||
|
},
|
||||||
|
models_ready: models_path.is_dir(),
|
||||||
|
models_count: total_model_files,
|
||||||
|
ffmpeg: std::process::Command::new("which")
|
||||||
|
.arg("ffmpeg")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false),
|
||||||
|
embedding_server: check_http("http://127.0.0.1:11436/health").await,
|
||||||
|
gdino_api: check_http("http://127.0.0.1:8080/health").await,
|
||||||
|
llm: check_http("http://127.0.0.1:8082/health").await,
|
||||||
|
rsync: check_rsync().await,
|
||||||
|
watcher_running: check_process_running("watcher"),
|
||||||
|
worker_running: check_process_running("worker"),
|
||||||
|
},
|
||||||
|
schema: check_schema_migrations(state.db.pool()).await,
|
||||||
|
identities: {
|
||||||
|
let identities_root = std::path::Path::new(&*config::OUTPUT_DIR).join("identities");
|
||||||
|
let directory_exists = identities_root.is_dir();
|
||||||
|
let files_count = crate::core::identity::storage::count_identity_files();
|
||||||
|
let index_ok = crate::core::identity::storage::read_index().is_ok();
|
||||||
|
let db_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM identities")
|
||||||
|
.fetch_one(state.db.pool())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
IdentityHealth {
|
||||||
|
directory_exists,
|
||||||
|
files_count,
|
||||||
|
index_ok,
|
||||||
|
db_count,
|
||||||
|
synced: directory_exists && files_count as i64 == db_count,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
integrations: IntegrationHealth {
|
||||||
|
tmdb: crate::core::tmdb::status::quick_status(),
|
||||||
|
},
|
||||||
|
config: ConfigHealth {
|
||||||
|
cache_enabled: config::get_cache_enabled(),
|
||||||
|
auto_pipeline_enabled: config::get_auto_pipeline_enabled(),
|
||||||
|
watcher_auto_register_enabled: config::get_watcher_auto_register(),
|
||||||
|
system_timezone: config::SYSTEM_TIMEZONE.clone(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_consistency(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<crate::core::health_agent::ConsistencyReport>, (StatusCode, String)> {
|
||||||
|
let report = crate::core::health_agent::run_consistency_checks(&state.db).await;
|
||||||
|
if report.checks.iter().any(|c| c.count > 0) {
|
||||||
|
tracing::warn!(
|
||||||
|
"[HEALTH] Consistency issues found: {}",
|
||||||
|
report
|
||||||
|
.checks
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.count > 0)
|
||||||
|
.map(|c| format!("{}={}", c.check, c.count))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Json(report))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_postgres() -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
match PostgresDb::init().await {
|
||||||
|
Ok(db) => match db.list_videos(1, 0).await {
|
||||||
|
Ok(_) => ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_redis() -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
match RedisClient::new() {
|
||||||
|
Ok(redis) => match redis.get_conn().await {
|
||||||
|
Ok(mut conn) => {
|
||||||
|
let result: Result<String, _> = redis::cmd("PING").query_async(&mut conn).await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_qdrant() -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
let base_url =
|
||||||
|
std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://localhost:6333".to_string());
|
||||||
|
let api_key =
|
||||||
|
std::env::var("QDRANT_API_KEY").unwrap_or_else(|_| "Test3200Test3200Test3200".to_string());
|
||||||
|
let url = format!("{}/collections", base_url);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
match client
|
||||||
|
.get(&url)
|
||||||
|
.header("api-key", api_key)
|
||||||
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resp) if resp.status().is_success() => ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Ok(resp) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(format!("HTTP {}", resp.status())),
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: None,
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_mongodb(cache: &MongoCache) -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
match cache.health_check().await {
|
||||||
|
Ok(_) => ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_required_migrations() -> Vec<MigrationInfo> {
|
||||||
|
let raw = env!("REQUIRED_MIGRATIONS");
|
||||||
|
if raw.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
raw.split(',')
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let mut parts = entry.splitn(2, ':');
|
||||||
|
let filename = parts.next()?.trim().to_string();
|
||||||
|
let checksum = parts.next()?.trim().to_string();
|
||||||
|
if filename.is_empty() || checksum.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(MigrationInfo { filename, checksum })
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_schema_migrations(pool: &sqlx::PgPool) -> SchemaHealth {
|
||||||
|
let required = parse_required_migrations();
|
||||||
|
|
||||||
|
let table_exists: bool = sqlx::query_scalar(
|
||||||
|
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'schema_migrations')",
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !table_exists {
|
||||||
|
return SchemaHealth {
|
||||||
|
table_exists: false,
|
||||||
|
applied: vec![],
|
||||||
|
required,
|
||||||
|
ok: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let applied: Vec<MigrationInfo> = sqlx::query_as::<_, (String, String)>(
|
||||||
|
"SELECT filename, checksum FROM schema_migrations ORDER BY id",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(filename, checksum)| MigrationInfo { filename, checksum })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let ok = required.iter().all(|req| {
|
||||||
|
applied
|
||||||
|
.iter()
|
||||||
|
.any(|app| app.filename == req.filename && app.checksum == req.checksum)
|
||||||
|
});
|
||||||
|
|
||||||
|
SchemaHealth {
|
||||||
|
table_exists: true,
|
||||||
|
applied,
|
||||||
|
required,
|
||||||
|
ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_rsync() -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
let paths = [
|
||||||
|
std::path::Path::new("/Users/accusys/bin/rsync"),
|
||||||
|
std::path::Path::new("/opt/homebrew/bin/rsync"),
|
||||||
|
];
|
||||||
|
for p in &paths {
|
||||||
|
if p.exists() {
|
||||||
|
return ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some("rsync not found (built from source expected at ~/bin/rsync)".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_process_running(name: &str) -> bool {
|
||||||
|
let patterns: &[&str] = match name {
|
||||||
|
"watcher" => &[
|
||||||
|
"target/release/momentry watcher",
|
||||||
|
"target/debug/momentry_playground watcher",
|
||||||
|
],
|
||||||
|
"worker" => &[
|
||||||
|
"target/release/momentry worker",
|
||||||
|
"target/debug/momentry_playground worker",
|
||||||
|
],
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
for pattern in patterns {
|
||||||
|
if let Ok(o) = std::process::Command::new("pgrep")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(pattern)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if o.status.success() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_http(url: &str) -> ServiceStatus {
|
||||||
|
let start = Instant::now();
|
||||||
|
match reqwest::get(url).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
ServiceStatus {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(format!("HTTP {}", resp.status())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => ServiceStatus {
|
||||||
|
status: "error".to_string(),
|
||||||
|
latency_ms: Some(start.elapsed().as_millis() as u64),
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn health_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/health", get(health))
|
||||||
|
.route("/health/detailed", get(health_detailed))
|
||||||
|
.route("/health/consistency", get(health_consistency))
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ pub struct CreateIdentityResponse {
|
|||||||
pub quality_avg: Option<f64>,
|
pub quality_avg: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
pub fn identity_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/identities", get(list_identities))
|
.route("/api/v1/identities", get(list_identities))
|
||||||
.route("/api/v1/identity", post(create_identity))
|
.route("/api/v1/identity", post(create_identity))
|
||||||
@@ -39,7 +39,7 @@ pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
|||||||
/// Register a Global Identity from face.json with multi-angle reference vectors.
|
/// Register a Global Identity from face.json with multi-angle reference vectors.
|
||||||
/// Calls select_face_reference_vectors_v2.py for automatic reference selection.
|
/// Calls select_face_reference_vectors_v2.py for automatic reference selection.
|
||||||
async fn create_identity(
|
async fn create_identity(
|
||||||
State(_state): State<crate::api::server::AppState>,
|
State(_state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<CreateIdentityRequest>,
|
Json(req): Json<CreateIdentityRequest>,
|
||||||
) -> Result<Json<CreateIdentityResponse>, (StatusCode, String)> {
|
) -> Result<Json<CreateIdentityResponse>, (StatusCode, String)> {
|
||||||
let schema = req.schema.unwrap_or("dev".to_string());
|
let schema = req.schema.unwrap_or("dev".to_string());
|
||||||
@@ -147,7 +147,7 @@ async fn create_identity(
|
|||||||
|
|
||||||
/// List all global identities
|
/// List all global identities
|
||||||
async fn list_identities(
|
async fn list_identities(
|
||||||
State(_state): State<crate::api::server::AppState>,
|
State(_state): State<crate::api::types::AppState>,
|
||||||
Query(query): Query<ListIdentitiesQuery>,
|
Query(query): Query<ListIdentitiesQuery>,
|
||||||
) -> Result<Json<IdentityListResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityListResponse>, (StatusCode, String)> {
|
||||||
let db = match PostgresDb::init().await {
|
let db = match PostgresDb::init().await {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::api::server::AppState;
|
use crate::api::types::AppState;
|
||||||
use crate::core::db::schema;
|
use crate::core::db::schema;
|
||||||
use crate::core::db::PostgresDb;
|
use crate::core::db::PostgresDb;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use sqlx::Row;
|
|||||||
|
|
||||||
use crate::core::db::ResourceRecord;
|
use crate::core::db::ResourceRecord;
|
||||||
|
|
||||||
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
pub fn identity_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/files", get(list_files))
|
.route("/api/v1/files", get(list_files))
|
||||||
.route("/api/v1/file/:file_uuid", get(get_file_detail))
|
.route("/api/v1/file/:file_uuid", get(get_file_detail))
|
||||||
@@ -65,7 +65,7 @@ pub struct FilesQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_files(
|
async fn list_files(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Query(params): Query<FilesQuery>,
|
Query(params): Query<FilesQuery>,
|
||||||
) -> Result<Json<FilesResponse>, (StatusCode, String)> {
|
) -> Result<Json<FilesResponse>, (StatusCode, String)> {
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
@@ -158,7 +158,7 @@ pub struct FileDetailResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_file_detail(
|
async fn get_file_detail(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
) -> Result<Json<FileDetailResponse>, (StatusCode, String)> {
|
) -> Result<Json<FileDetailResponse>, (StatusCode, String)> {
|
||||||
let file = state
|
let file = state
|
||||||
@@ -212,7 +212,7 @@ pub struct FileIdentityItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_file_identities(
|
async fn get_file_identities(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Query(params): Query<FilesQuery>,
|
Query(params): Query<FilesQuery>,
|
||||||
) -> Result<Json<FileIdentitiesResponse>, (StatusCode, String)> {
|
) -> Result<Json<FileIdentitiesResponse>, (StatusCode, String)> {
|
||||||
@@ -300,7 +300,7 @@ fn strip_uuid(u: &uuid::Uuid) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_detail(
|
async fn get_identity_detail(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
) -> Result<Json<IdentityDetailResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityDetailResponse>, (StatusCode, String)> {
|
||||||
let uuid_clean = identity_uuid.replace('-', "");
|
let uuid_clean = identity_uuid.replace('-', "");
|
||||||
@@ -338,7 +338,7 @@ async fn get_identity_detail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_status(
|
async fn get_identity_status(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
) -> Result<Json<IdentityStatusResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityStatusResponse>, (StatusCode, String)> {
|
||||||
let uuid_clean = identity_uuid.replace('-', "");
|
let uuid_clean = identity_uuid.replace('-', "");
|
||||||
@@ -386,7 +386,7 @@ pub struct IdentityFilesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_identity(
|
async fn delete_identity(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
let table = crate::core::db::schema::table_name("face_detections");
|
let table = crate::core::db::schema::table_name("face_detections");
|
||||||
@@ -438,7 +438,7 @@ pub struct IdentityFileItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_files(
|
async fn get_identity_files(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
Query(params): Query<FilesQuery>,
|
Query(params): Query<FilesQuery>,
|
||||||
) -> Result<Json<IdentityFilesResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityFilesResponse>, (StatusCode, String)> {
|
||||||
@@ -524,7 +524,7 @@ pub struct BBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_faces(
|
async fn get_identity_faces(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
Query(params): Query<FilesQuery>,
|
Query(params): Query<FilesQuery>,
|
||||||
) -> Result<Json<IdentityFacesResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityFacesResponse>, (StatusCode, String)> {
|
||||||
@@ -608,7 +608,7 @@ pub struct IdentityChunkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_chunks(
|
async fn get_identity_chunks(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
Query(params): Query<FilesQuery>,
|
Query(params): Query<FilesQuery>,
|
||||||
) -> Result<Json<IdentityChunksResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityChunksResponse>, (StatusCode, String)> {
|
||||||
@@ -680,7 +680,7 @@ pub struct ResourceItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn register_resource(
|
async fn register_resource(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<RegisterResourceRequest>,
|
Json(req): Json<RegisterResourceRequest>,
|
||||||
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
||||||
let resource = ResourceRecord {
|
let resource = ResourceRecord {
|
||||||
@@ -715,7 +715,7 @@ pub struct HeartbeatRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn heartbeat_resource(
|
async fn heartbeat_resource(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<HeartbeatRequest>,
|
Json(req): Json<HeartbeatRequest>,
|
||||||
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
||||||
let status = req.status.unwrap_or("online".to_string());
|
let status = req.status.unwrap_or("online".to_string());
|
||||||
@@ -733,7 +733,7 @@ async fn heartbeat_resource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_resources(
|
async fn list_resources(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
||||||
let records = state
|
let records = state
|
||||||
.db
|
.db
|
||||||
@@ -771,7 +771,7 @@ struct IdentityUploadResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_identity(
|
async fn upload_identity(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Json(payload): Json<crate::core::identity::storage::IdentityFile>,
|
Json(payload): Json<crate::core::identity::storage::IdentityFile>,
|
||||||
) -> Result<Json<IdentityUploadResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<IdentityUploadResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let parsed = uuid::Uuid::parse_str(&payload.identity_uuid)
|
let parsed = uuid::Uuid::parse_str(&payload.identity_uuid)
|
||||||
@@ -838,7 +838,7 @@ struct ProfileImageResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_profile_image(
|
async fn upload_profile_image(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> Result<Json<ProfileImageResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<ProfileImageResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
@@ -951,7 +951,7 @@ async fn get_profile_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_identity_json(
|
async fn get_identity_json(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
) -> Result<(StatusCode, [(String, String); 1], Vec<u8>), StatusCode> {
|
) -> Result<(StatusCode, [(String, String); 1], Vec<u8>), StatusCode> {
|
||||||
let clean = identity_uuid.replace('-', "");
|
let clean = identity_uuid.replace('-', "");
|
||||||
@@ -1040,7 +1040,7 @@ struct IdentityTextResponse {
|
|||||||
|
|
||||||
/// Path A: Search chunk text → associated identities
|
/// Path A: Search chunk text → associated identities
|
||||||
async fn search_identity_text(
|
async fn search_identity_text(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Query(params): Query<IdentityTextQuery>,
|
Query(params): Query<IdentityTextQuery>,
|
||||||
) -> Result<Json<IdentityTextResponse>, StatusCode> {
|
) -> Result<Json<IdentityTextResponse>, StatusCode> {
|
||||||
use crate::core::db::schema;
|
use crate::core::db::schema;
|
||||||
@@ -1148,7 +1148,7 @@ struct IdentitySearchResponse {
|
|||||||
|
|
||||||
/// Path B: Search identity name → associated chunk text
|
/// Path B: Search identity name → associated chunk text
|
||||||
async fn search_identities_by_text(
|
async fn search_identities_by_text(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Query(params): Query<IdentitySearchQuery>,
|
Query(params): Query<IdentitySearchQuery>,
|
||||||
) -> Result<Json<IdentitySearchResponse>, StatusCode> {
|
) -> Result<Json<IdentitySearchResponse>, StatusCode> {
|
||||||
use crate::core::db::schema;
|
use crate::core::db::schema;
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ pub struct IdentityTracesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_identity_traces(
|
pub async fn get_identity_traces(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(identity_uuid): Path<String>,
|
Path(identity_uuid): Path<String>,
|
||||||
) -> Result<Json<IdentityTracesResponse>, (StatusCode, String)> {
|
) -> Result<Json<IdentityTracesResponse>, (StatusCode, String)> {
|
||||||
let id_table = crate::core::db::schema::table_name("identities");
|
let id_table = crate::core::db::schema::table_name("identities");
|
||||||
@@ -458,7 +458,7 @@ pub async fn get_identity_traces(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn identity_binding_routes() -> Router<crate::api::server::AppState> {
|
pub fn identity_binding_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/identity/:identity_uuid/bind", post(bind_identity))
|
.route("/api/v1/identity/:identity_uuid/bind", post(bind_identity))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fn ffmpeg_cmd() -> std::process::Command {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bbox_routes() -> Router<crate::api::server::AppState> {
|
pub fn bbox_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/file/:file_uuid/video/bbox",
|
"/api/v1/file/:file_uuid/video/bbox",
|
||||||
@@ -168,7 +168,7 @@ fn resolve_frame_range(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn bbox_overlay_video(
|
async fn bbox_overlay_video(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Query(p): Query<BboxParams>,
|
Query(p): Query<BboxParams>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
@@ -297,7 +297,7 @@ fn parse_range(range: &str, file_size: u64) -> (u64, u64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn trace_video(
|
async fn trace_video(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path((file_uuid, trace_id)): Path<(String, i32)>,
|
Path((file_uuid, trace_id)): Path<(String, i32)>,
|
||||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
@@ -554,7 +554,7 @@ async fn trace_video(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn stream_video(
|
async fn stream_video(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||||
request: axum::http::Request<Body>,
|
request: axum::http::Request<Body>,
|
||||||
@@ -698,7 +698,7 @@ struct ThumbQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn face_thumbnail(
|
async fn face_thumbnail(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Query(q): Query<ThumbQuery>,
|
Query(q): Query<ThumbQuery>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
@@ -761,7 +761,7 @@ struct ClipQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn video_clip(
|
async fn video_clip(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Query(q): Query<ClipQuery>,
|
Query(q): Query<ClipQuery>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
pub mod agent_api;
|
pub mod agent_api;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod docs;
|
||||||
|
pub mod files;
|
||||||
pub mod five_w1h_agent_api;
|
pub mod five_w1h_agent_api;
|
||||||
|
pub mod health;
|
||||||
pub mod identities;
|
pub mod identities;
|
||||||
pub mod identity_agent_api;
|
pub mod identity_agent_api;
|
||||||
pub mod identity_api;
|
pub mod identity_api;
|
||||||
pub mod identity_binding;
|
pub mod identity_binding;
|
||||||
pub mod media_api;
|
pub mod media_api;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
pub mod scan;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmdb_api;
|
pub mod tmdb_api;
|
||||||
pub mod trace_agent_api;
|
pub mod trace_agent_api;
|
||||||
|
pub mod types;
|
||||||
pub mod universal_search;
|
pub mod universal_search;
|
||||||
pub mod visual_chunk_search;
|
pub mod visual_chunk_search;
|
||||||
|
pub mod visual_search;
|
||||||
|
|
||||||
pub use server::start_server;
|
pub use server::start_server;
|
||||||
|
|||||||
513
src/api/processing.rs
Normal file
513
src/api/processing.rs
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Json,
|
||||||
|
routing::post,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use super::types::AppState;
|
||||||
|
use crate::core::cache::{keys, RedisCache};
|
||||||
|
use crate::core::config::REDIS_KEY_PREFIX;
|
||||||
|
use crate::core::db::schema;
|
||||||
|
use crate::core::db::{Database, MonitorJobStatus, PostgresDb, RedisClient, VideoRecord, VideoStatus};
|
||||||
|
use crate::core::probe::ffprobe;
|
||||||
|
use crate::worker::processor;
|
||||||
|
use crate::{Embedder, FileManager};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JobListResponse {
|
||||||
|
jobs: Vec<JobInfoResponse>,
|
||||||
|
count: i64,
|
||||||
|
page: usize,
|
||||||
|
page_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct JobsQuery {
|
||||||
|
page: Option<usize>,
|
||||||
|
page_size: Option<usize>,
|
||||||
|
status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JobInfoResponse {
|
||||||
|
id: i32,
|
||||||
|
uuid: String,
|
||||||
|
status: String,
|
||||||
|
current_processor: Option<String>,
|
||||||
|
progress_current: i32,
|
||||||
|
progress_total: i32,
|
||||||
|
created_at: String,
|
||||||
|
started_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JobDetailResponse {
|
||||||
|
id: i32,
|
||||||
|
uuid: String,
|
||||||
|
status: String,
|
||||||
|
current_processor: Option<String>,
|
||||||
|
progress_current: i32,
|
||||||
|
progress_total: i32,
|
||||||
|
processors: Vec<ProcessorInfoResponse>,
|
||||||
|
created_at: String,
|
||||||
|
started_at: Option<String>,
|
||||||
|
updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ProcessorInfoResponse {
|
||||||
|
processor_type: String,
|
||||||
|
status: String,
|
||||||
|
started_at: Option<String>,
|
||||||
|
completed_at: Option<String>,
|
||||||
|
duration_secs: Option<f64>,
|
||||||
|
error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CacheToggleRequest {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CacheToggleResponse {
|
||||||
|
success: bool,
|
||||||
|
cache_enabled: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AutoPipelineToggleRequest {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AutoPipelineToggleResponse {
|
||||||
|
success: bool,
|
||||||
|
auto_pipeline_enabled: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct WatcherAutoRegisterToggleRequest {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct WatcherAutoRegisterToggleResponse {
|
||||||
|
success: bool,
|
||||||
|
watcher_auto_register_enabled: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ProcessRequest {
|
||||||
|
rules: Option<Vec<String>>,
|
||||||
|
processors: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ProgressResponse {
|
||||||
|
file_uuid: String,
|
||||||
|
user: Option<String>,
|
||||||
|
group: Option<String>,
|
||||||
|
file_name: Option<String>,
|
||||||
|
duration: Option<f64>,
|
||||||
|
overall_progress: u32,
|
||||||
|
cpu_percent: Option<f64>,
|
||||||
|
gpu_percent: Option<f64>,
|
||||||
|
memory_percent: Option<f64>,
|
||||||
|
memory_mb: Option<u64>,
|
||||||
|
system: Option<SystemHealthInfo>,
|
||||||
|
processors: Vec<ProcessorProgressInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct SystemHealthInfo {
|
||||||
|
cpu_idle_pct: f64,
|
||||||
|
memory_available_mb: u64,
|
||||||
|
memory_total_mb: u64,
|
||||||
|
memory_used_pct: f64,
|
||||||
|
gpu_available: bool,
|
||||||
|
gpu_utilization_pct: Option<f64>,
|
||||||
|
gpu_memory_used_pct: Option<f64>,
|
||||||
|
dynamic_concurrency: u32,
|
||||||
|
config_concurrency: u32,
|
||||||
|
running_processors: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ProcessorProgressInfo {
|
||||||
|
name: String,
|
||||||
|
status: String,
|
||||||
|
current: u32,
|
||||||
|
total: u32,
|
||||||
|
progress: u32,
|
||||||
|
message: String,
|
||||||
|
frames_processed: i32,
|
||||||
|
chunks_produced: i32,
|
||||||
|
retry_count: i32,
|
||||||
|
eta_seconds: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_system_stats() -> (Option<f64>, Option<f64>, Option<f64>, Option<u64>) {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let pid = std::process::id().to_string();
|
||||||
|
|
||||||
|
let cpu = Command::new("ps")
|
||||||
|
.args(["-p", &pid, "-o", "%cpu="])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok());
|
||||||
|
|
||||||
|
let (mem_percent, mem_rss) = Command::new("ps")
|
||||||
|
.args(["-p", &pid, "-o", "%mem=,rss="])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.map(|o| {
|
||||||
|
let output = String::from_utf8_lossy(&o.stdout);
|
||||||
|
let parts: Vec<&str> = output.split_whitespace().collect();
|
||||||
|
let percent = parts.first().and_then(|s| s.parse().ok());
|
||||||
|
let rss = parts.get(1).and_then(|s| s.parse().ok());
|
||||||
|
(percent, rss)
|
||||||
|
})
|
||||||
|
.unwrap_or((None, None));
|
||||||
|
|
||||||
|
let gpu = Command::new("nvidia-smi")
|
||||||
|
.args(["--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok());
|
||||||
|
|
||||||
|
let mem_mb = mem_rss.map(|r: u64| r / 1024);
|
||||||
|
|
||||||
|
(cpu, mem_percent, gpu, mem_mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn trigger_processing(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(uuid): Path<String>,
|
||||||
|
Json(req): Json<ProcessRequest>,
|
||||||
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
let videos_table = schema::table_name("videos");
|
||||||
|
let row: Option<(String, String, String, Option<String>, String, Option<String>)> =
|
||||||
|
sqlx::query_as(&format!(
|
||||||
|
"SELECT file_uuid, file_path, file_name, file_type, COALESCE(processing_status, 'REGISTERED'), content_hash FROM {} WHERE file_uuid = $1",
|
||||||
|
videos_table
|
||||||
|
))
|
||||||
|
.bind(&uuid)
|
||||||
|
.fetch_optional(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("DB error: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (file_uuid, file_path, file_name, file_type, processing_status, content_hash) =
|
||||||
|
row.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
if processing_status == "PROCESSING" || processing_status == "QUEUED" {
|
||||||
|
return Err(StatusCode::CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
||||||
|
.unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string());
|
||||||
|
|
||||||
|
let status_update = format!(
|
||||||
|
"UPDATE {} SET processing_status = 'QUEUED' WHERE file_uuid = $1",
|
||||||
|
videos_table
|
||||||
|
);
|
||||||
|
sqlx::query(&status_update)
|
||||||
|
.bind(&file_uuid)
|
||||||
|
.execute(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let output_path = std::path::Path::new(&output_dir).join(format!("{}.monitor.json", file_uuid));
|
||||||
|
|
||||||
|
let monitor_jobs_table = schema::table_name("monitor_jobs");
|
||||||
|
let redis = RedisClient::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let mut conn = redis.get_conn().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let processors_to_run: Vec<&str> = if let Some(procs) = &req.processors {
|
||||||
|
let procs_str = serde_json::to_string(procs).unwrap_or_default();
|
||||||
|
sqlx::query(&format!("UPDATE {} SET processors = $1 WHERE id = $2", schema::table_name("monitor_jobs")))
|
||||||
|
.bind(&procs_str)
|
||||||
|
.bind(&file_uuid)
|
||||||
|
.execute(state.db.pool())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
procs.iter().map(|s| s.as_str()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
let notification = serde_json::json!({
|
||||||
|
"action": "process",
|
||||||
|
"file_uuid": file_uuid,
|
||||||
|
"file_path": file_path,
|
||||||
|
"file_name": file_name,
|
||||||
|
"file_type": file_type,
|
||||||
|
"content_hash": content_hash,
|
||||||
|
"output_dir": output_dir,
|
||||||
|
"processors": processors_to_run,
|
||||||
|
});
|
||||||
|
|
||||||
|
let notification_key = format!("{}notifications", REDIS_KEY_PREFIX.as_str());
|
||||||
|
let _: Result<(), _> = redis::cmd("PUBLISH")
|
||||||
|
.arg(¬ification_key)
|
||||||
|
.arg(notification.to_string())
|
||||||
|
.query_async(&mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tracing::info!("[TRIGGER] Published processing notification for {}", file_uuid);
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Processing queued",
|
||||||
|
"file_uuid": file_uuid,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_json(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((file_uuid, processor)): Path<(String, String)>,
|
||||||
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
||||||
|
.unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string());
|
||||||
|
let path = std::path::Path::new(&output_dir).join(format!("{}.{}.json", file_uuid, processor));
|
||||||
|
match tokio::fs::read_to_string(&path).await {
|
||||||
|
Ok(content) => serde_json::from_str(&content).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
Err(_) => Err(StatusCode::NOT_FOUND),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_chunk_by_path(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((file_uuid, chunk_id)): Path<(String, String)>,
|
||||||
|
) -> Result<Json<crate::core::chunk::types::Chunk>, StatusCode> {
|
||||||
|
let table = schema::table_name("chunk");
|
||||||
|
let chunk: Option<crate::core::chunk::types::Chunk> = sqlx::query_as(&format!(
|
||||||
|
"SELECT * FROM {} WHERE uuid = $1 AND chunk_id = $2",
|
||||||
|
table
|
||||||
|
))
|
||||||
|
.bind(&file_uuid)
|
||||||
|
.bind(&chunk_id)
|
||||||
|
.fetch_optional(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
chunk.ok_or(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_progress(
|
||||||
|
file_uuid: Path<String>,
|
||||||
|
) -> Result<Json<ProgressResponse>, StatusCode> {
|
||||||
|
let file_uuid = file_uuid.0;
|
||||||
|
let redis = RedisClient::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let mut conn = redis.get_conn().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let key = format!("{}progress:{}", REDIS_KEY_PREFIX.as_str(), file_uuid);
|
||||||
|
let progress_json: Option<String> = redis::cmd("GET")
|
||||||
|
.arg(&key)
|
||||||
|
.query_async(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let (cpu, mem_pct, gpu, mem_mb) = get_system_stats();
|
||||||
|
let sys = SystemHealthInfo {
|
||||||
|
cpu_idle_pct: cpu.map(|c: f64| 100.0 - c).unwrap_or(0.0),
|
||||||
|
memory_available_mb: mem_mb.unwrap_or(0),
|
||||||
|
memory_total_mb: 0,
|
||||||
|
memory_used_pct: mem_pct.unwrap_or(0.0),
|
||||||
|
gpu_available: gpu.is_some(),
|
||||||
|
gpu_utilization_pct: gpu,
|
||||||
|
gpu_memory_used_pct: None,
|
||||||
|
dynamic_concurrency: 0,
|
||||||
|
config_concurrency: 0,
|
||||||
|
running_processors: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pg = PostgresDb::init().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let video = pg.get_video_by_uuid(&file_uuid).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let (overall, processors) = if let Some(json_str) = &progress_json {
|
||||||
|
match serde_json::from_str::<ProgressResponse>(json_str) {
|
||||||
|
Ok(p) => (p.overall_progress, p.processors),
|
||||||
|
Err(_) => (0u32, vec![]),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(0u32, vec![])
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(ProgressResponse {
|
||||||
|
file_uuid,
|
||||||
|
user: None,
|
||||||
|
group: None,
|
||||||
|
file_name: video.as_ref().map(|v| v.file_name.clone()),
|
||||||
|
duration: video.as_ref().map(|v| v.duration),
|
||||||
|
overall_progress: overall,
|
||||||
|
cpu_percent: cpu,
|
||||||
|
gpu_percent: gpu,
|
||||||
|
memory_percent: mem_pct,
|
||||||
|
memory_mb,
|
||||||
|
system: Some(sys),
|
||||||
|
processors,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_jobs(
|
||||||
|
Query(params): Query<JobsQuery>,
|
||||||
|
) -> Result<Json<JobListResponse>, StatusCode> {
|
||||||
|
let pg = PostgresDb::init().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let jobs_table = schema::table_name("monitor_jobs");
|
||||||
|
let videos_table = schema::table_name("videos");
|
||||||
|
|
||||||
|
let page = params.page.unwrap_or(1).max(1);
|
||||||
|
let page_size = params.page_size.unwrap_or(20).max(1).min(100);
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
|
||||||
|
let mut where_clause = String::new();
|
||||||
|
if let Some(ref status) = params.status {
|
||||||
|
where_clause = format!(" WHERE j.status = '{}'", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count: i64 = sqlx::query_scalar(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} j{}",
|
||||||
|
jobs_table, where_clause
|
||||||
|
))
|
||||||
|
.fetch_one(pg.pool())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let jobs: Vec<(i32, String, String, String, String, Option<String>, i32, i32)> =
|
||||||
|
sqlx::query_as(&format!(
|
||||||
|
"SELECT j.id::int, j.uuid, v.file_name, COALESCE(j.status, 'QUEUED'), COALESCE(j.current_processor, ''), v.file_uuid, COALESCE(j.processed_frames, 0), COALESCE(j.total_frames, 0) \
|
||||||
|
FROM {} j LEFT JOIN {} v ON v.file_uuid = j.uuid{} \
|
||||||
|
ORDER BY j.id DESC LIMIT $1 OFFSET $2",
|
||||||
|
jobs_table, videos_table, where_clause
|
||||||
|
))
|
||||||
|
.bind(page_size as i64)
|
||||||
|
.bind(offset as i64)
|
||||||
|
.fetch_all(pg.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let job_list = jobs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, uuid, fname, status, cp, _vuuid, pf, tf)| JobInfoResponse {
|
||||||
|
id,
|
||||||
|
uuid,
|
||||||
|
status,
|
||||||
|
current_processor: Some(cp),
|
||||||
|
progress_current: pf,
|
||||||
|
progress_total: tf,
|
||||||
|
created_at: String::new(),
|
||||||
|
started_at: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(JobListResponse {
|
||||||
|
jobs: job_list,
|
||||||
|
count,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job(
|
||||||
|
Path(uuid): Path<String>,
|
||||||
|
) -> Result<Json<JobDetailResponse>, StatusCode> {
|
||||||
|
let pg = PostgresDb::init().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let jobs_table = schema::table_name("monitor_jobs");
|
||||||
|
let videos_table = schema::table_name("videos");
|
||||||
|
|
||||||
|
let job: Option<(i32, String, String, String, Option<String>, i32, i32, String, Option<String>, Option<String>)> =
|
||||||
|
sqlx::query_as(&format!(
|
||||||
|
"SELECT j.id::int, j.uuid, COALESCE(v.file_name, 'unknown'), COALESCE(j.status, 'QUEUED'), j.current_processor, \
|
||||||
|
COALESCE(j.processed_frames, 0), COALESCE(j.total_frames, 0), \
|
||||||
|
COALESCE(j.created_at::text, ''), j.started_at::text, j.updated_at::text \
|
||||||
|
FROM {} j LEFT JOIN {} v ON v.file_uuid = j.uuid WHERE j.uuid = $1",
|
||||||
|
jobs_table, videos_table
|
||||||
|
))
|
||||||
|
.bind(&uuid)
|
||||||
|
.fetch_optional(pg.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let (id, uuid, file_name, status, current_processor, pf, tf, created_at, started_at, updated_at) =
|
||||||
|
job.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
Ok(Json(JobDetailResponse {
|
||||||
|
id,
|
||||||
|
uuid,
|
||||||
|
status,
|
||||||
|
current_processor,
|
||||||
|
progress_current: pf,
|
||||||
|
progress_total: tf,
|
||||||
|
processors: vec![],
|
||||||
|
created_at,
|
||||||
|
started_at,
|
||||||
|
updated_at,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cache_toggle(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<CacheToggleRequest>,
|
||||||
|
) -> Json<CacheToggleResponse> {
|
||||||
|
crate::core::config::set_cache_enabled(req.enabled);
|
||||||
|
if !req.enabled {
|
||||||
|
let _ = state.mongo_cache.flush_all().await;
|
||||||
|
let _ = state.redis_cache.flush().await;
|
||||||
|
}
|
||||||
|
Json(CacheToggleResponse {
|
||||||
|
success: true,
|
||||||
|
cache_enabled: req.enabled,
|
||||||
|
message: format!("Cache {}", if req.enabled { "enabled" } else { "disabled" }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auto_pipeline_toggle(
|
||||||
|
Json(req): Json<AutoPipelineToggleRequest>,
|
||||||
|
) -> Json<AutoPipelineToggleResponse> {
|
||||||
|
crate::core::config::set_auto_pipeline_enabled(req.enabled);
|
||||||
|
Json(AutoPipelineToggleResponse {
|
||||||
|
success: true,
|
||||||
|
auto_pipeline_enabled: req.enabled,
|
||||||
|
message: format!(
|
||||||
|
"Auto pipeline {}",
|
||||||
|
if req.enabled { "enabled" } else { "disabled" }
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn watcher_auto_register_toggle(
|
||||||
|
Json(req): Json<WatcherAutoRegisterToggleRequest>,
|
||||||
|
) -> Json<WatcherAutoRegisterToggleResponse> {
|
||||||
|
crate::core::config::set_watcher_auto_register(req.enabled);
|
||||||
|
Json(WatcherAutoRegisterToggleResponse {
|
||||||
|
success: true,
|
||||||
|
watcher_auto_register_enabled: req.enabled,
|
||||||
|
message: format!(
|
||||||
|
"Watcher auto-register {}",
|
||||||
|
if req.enabled { "enabled" } else { "disabled" }
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn processing_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/v1/file/:file_uuid/process", post(trigger_processing))
|
||||||
|
.route("/api/v1/file/:file_uuid/json/:processor", post(download_json))
|
||||||
|
.route("/api/v1/file/:file_uuid/chunk/:chunk_id", post(get_chunk_by_path))
|
||||||
|
.route("/api/v1/progress/:file_uuid", post(get_progress))
|
||||||
|
.route("/api/v1/jobs", post(list_jobs))
|
||||||
|
.route("/api/v1/config/cache", post(cache_toggle))
|
||||||
|
.route("/api/v1/config/auto-pipeline", post(auto_pipeline_toggle))
|
||||||
|
.route("/api/v1/config/watcher-auto-register", post(watcher_auto_register_toggle))
|
||||||
|
}
|
||||||
517
src/api/scan.rs
Normal file
517
src/api/scan.rs
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Json,
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::types::AppState;
|
||||||
|
use crate::core::db::schema;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ScannedFileInfo {
|
||||||
|
file_name: String,
|
||||||
|
relative_path: String,
|
||||||
|
file_path: String,
|
||||||
|
file_size: u64,
|
||||||
|
modified_time: String,
|
||||||
|
is_registered: bool,
|
||||||
|
file_uuid: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
registration_time: Option<String>,
|
||||||
|
job_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ScanFilesResponse {
|
||||||
|
files: Vec<ScannedFileInfo>,
|
||||||
|
total: usize,
|
||||||
|
filtered_total: usize,
|
||||||
|
page: usize,
|
||||||
|
page_size: usize,
|
||||||
|
total_pages: usize,
|
||||||
|
registered_count: usize,
|
||||||
|
unregistered_count: usize,
|
||||||
|
total_chunks: i64,
|
||||||
|
searchable_chunks: i64,
|
||||||
|
pending_videos: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ScanFilesQuery {
|
||||||
|
limit: Option<usize>,
|
||||||
|
page: Option<usize>,
|
||||||
|
page_size: Option<usize>,
|
||||||
|
pattern: Option<String>,
|
||||||
|
sort_by: Option<String>,
|
||||||
|
sort_order: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_directory_recursive(
|
||||||
|
dir: &std::path::Path,
|
||||||
|
root: &std::path::Path,
|
||||||
|
allowed_extensions: &[&str],
|
||||||
|
registered_paths: &std::collections::HashMap<
|
||||||
|
String,
|
||||||
|
(String, String, Option<String>, Option<i32>),
|
||||||
|
>,
|
||||||
|
files: &mut Vec<ScannedFileInfo>,
|
||||||
|
) {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if name.starts_with('.') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scan_directory_recursive(&path, root, allowed_extensions, registered_paths, files);
|
||||||
|
} else if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||||
|
if allowed_extensions.contains(&ext.to_lowercase().as_str()) {
|
||||||
|
if let Ok(meta) = entry.metadata() {
|
||||||
|
let abs_path = path.to_string_lossy().to_string();
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(root)
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| abs_path.clone());
|
||||||
|
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let modified_time = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|d| {
|
||||||
|
chrono::DateTime::from_timestamp(d.as_secs() as i64, 0)
|
||||||
|
.map(|dt| dt.to_rfc3339())
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
match registered_paths.get(&abs_path) {
|
||||||
|
Some((uuid, status, reg_time, jid)) if status != "unregistered" => {
|
||||||
|
files.push(ScannedFileInfo {
|
||||||
|
file_name,
|
||||||
|
relative_path: rel_path,
|
||||||
|
file_path: abs_path,
|
||||||
|
file_size: meta.len(),
|
||||||
|
modified_time,
|
||||||
|
is_registered: true,
|
||||||
|
file_uuid: Some(uuid.clone()),
|
||||||
|
status: Some(status.clone()),
|
||||||
|
registration_time: reg_time.clone(),
|
||||||
|
job_id: *jid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
files.push(ScannedFileInfo {
|
||||||
|
file_name,
|
||||||
|
relative_path: rel_path,
|
||||||
|
file_path: abs_path,
|
||||||
|
file_size: meta.len(),
|
||||||
|
modified_time,
|
||||||
|
is_registered: false,
|
||||||
|
file_uuid: None,
|
||||||
|
status: Some("unregistered".to_string()),
|
||||||
|
registration_time: None,
|
||||||
|
job_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scan_files(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(params): Query<ScanFilesQuery>,
|
||||||
|
) -> Result<Json<ScanFilesResponse>, StatusCode> {
|
||||||
|
let demo_dir_str = std::env::var("MOMENTRY_SFTP_ROOT")
|
||||||
|
.unwrap_or_else(|_| "/Users/accusys/momentry/var/sftpgo/data/demo".to_string());
|
||||||
|
let demo_dir = std::path::Path::new(&demo_dir_str);
|
||||||
|
|
||||||
|
let allowed_extensions = vec![
|
||||||
|
"mp4", "mov", "mkv", "avi", "webm", "jpg", "jpeg", "png", "gif", "webp",
|
||||||
|
];
|
||||||
|
|
||||||
|
let table = schema::table_name("videos");
|
||||||
|
let mj_table = schema::table_name("monitor_jobs");
|
||||||
|
let registered_db: Vec<(String, String, String, String, Option<String>, Option<i32>)> =
|
||||||
|
sqlx::query_as(&format!(
|
||||||
|
"SELECT v.file_path, v.file_name, v.file_uuid, v.status, v.registration_time::text, \
|
||||||
|
latest_job.id as job_id \
|
||||||
|
FROM {} v \
|
||||||
|
LEFT JOIN LATERAL ( \
|
||||||
|
SELECT id FROM {} WHERE uuid = v.file_uuid ORDER BY id DESC LIMIT 1 \
|
||||||
|
) latest_job ON true \
|
||||||
|
ORDER BY v.id",
|
||||||
|
table, mj_table
|
||||||
|
))
|
||||||
|
.fetch_all(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let registered_paths: std::collections::HashMap<
|
||||||
|
String,
|
||||||
|
(String, String, Option<String>, Option<i32>),
|
||||||
|
> = registered_db
|
||||||
|
.into_iter()
|
||||||
|
.map(|(path, _name, uuid, status, reg_time, jid)| (path, (uuid, status, reg_time, jid)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut result_files = Vec::new();
|
||||||
|
|
||||||
|
if demo_dir.exists() {
|
||||||
|
scan_directory_recursive(
|
||||||
|
demo_dir,
|
||||||
|
demo_dir,
|
||||||
|
&allowed_extensions,
|
||||||
|
®istered_paths,
|
||||||
|
&mut result_files,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let desc = params.sort_order.as_deref().unwrap_or("asc") == "desc";
|
||||||
|
match params.sort_by.as_deref().unwrap_or("name") {
|
||||||
|
"size" => {
|
||||||
|
if desc {
|
||||||
|
result_files.sort_by(|a, b| b.file_size.cmp(&a.file_size));
|
||||||
|
} else {
|
||||||
|
result_files.sort_by(|a, b| a.file_size.cmp(&b.file_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"modified" | "time" => {
|
||||||
|
if desc {
|
||||||
|
result_files.sort_by(|a, b| b.modified_time.cmp(&a.modified_time));
|
||||||
|
} else {
|
||||||
|
result_files.sort_by(|a, b| a.modified_time.cmp(&b.modified_time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status" => {
|
||||||
|
if desc {
|
||||||
|
result_files
|
||||||
|
.sort_by(|a, b| b.status.cmp(&a.status).then(b.file_name.cmp(&a.file_name)));
|
||||||
|
} else {
|
||||||
|
result_files
|
||||||
|
.sort_by(|a, b| a.status.cmp(&b.status).then(a.file_name.cmp(&b.file_name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if desc {
|
||||||
|
result_files.sort_by(|a, b| {
|
||||||
|
a.is_registered
|
||||||
|
.cmp(&b.is_registered)
|
||||||
|
.then(b.file_name.cmp(&a.file_name))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result_files.sort_by(|a, b| {
|
||||||
|
b.is_registered
|
||||||
|
.cmp(&a.is_registered)
|
||||||
|
.then(a.file_name.cmp(&b.file_name))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_all = result_files.len();
|
||||||
|
let registered_count = result_files.iter().filter(|f| f.is_registered).count();
|
||||||
|
let unregistered_count = result_files.iter().filter(|f| !f.is_registered).count();
|
||||||
|
|
||||||
|
let filtered: Vec<ScannedFileInfo> = if let Some(ref pat) = params.pattern {
|
||||||
|
let re = match regex::Regex::new(&format!("(?i){}", pat)) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return Err(StatusCode::BAD_REQUEST),
|
||||||
|
};
|
||||||
|
result_files
|
||||||
|
.into_iter()
|
||||||
|
.filter(|f| re.is_match(&f.file_name))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
result_files
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtered_total = filtered.len();
|
||||||
|
|
||||||
|
let page = params.page.unwrap_or(1).max(1);
|
||||||
|
let page_size = params
|
||||||
|
.page_size
|
||||||
|
.or(params.limit)
|
||||||
|
.unwrap_or(filtered_total.max(1));
|
||||||
|
let total_pages = if page_size > 0 {
|
||||||
|
(filtered_total + page_size - 1) / page_size
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let start = (page - 1) * page_size;
|
||||||
|
let files: Vec<ScannedFileInfo> = filtered.into_iter().skip(start).take(page_size).collect();
|
||||||
|
|
||||||
|
let table_videos = schema::table_name("videos");
|
||||||
|
let table_chunks = schema::table_name("chunk");
|
||||||
|
let total_chunks: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {}", table_chunks))
|
||||||
|
.fetch_one(state.db.pool())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
let searchable_chunks: i64 = sqlx::query_scalar(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} WHERE vector_id IS NOT NULL",
|
||||||
|
table_chunks
|
||||||
|
))
|
||||||
|
.fetch_one(state.db.pool())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
let pending_videos: i64 = sqlx::query_scalar(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} WHERE status = 'pending'",
|
||||||
|
table_videos
|
||||||
|
))
|
||||||
|
.fetch_one(state.db.pool())
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(Json(ScanFilesResponse {
|
||||||
|
files,
|
||||||
|
total: total_all,
|
||||||
|
filtered_total,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
total_pages,
|
||||||
|
registered_count,
|
||||||
|
unregistered_count,
|
||||||
|
total_chunks,
|
||||||
|
searchable_chunks,
|
||||||
|
pending_videos,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct SftpgoStatusResponse {
|
||||||
|
username: String,
|
||||||
|
home_dir: String,
|
||||||
|
files_count: i64,
|
||||||
|
registered_videos: Vec<RegisteredVideo>,
|
||||||
|
last_login: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RegisteredVideo {
|
||||||
|
uuid: String,
|
||||||
|
file_name: String,
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_sftpgo_status(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<SftpgoStatusResponse>, StatusCode> {
|
||||||
|
let demo_dir = "/Users/accusys/momentry/var/sftpgo/data/demo";
|
||||||
|
|
||||||
|
let files_count: i64 = std::fs::read_dir(demo_dir)
|
||||||
|
.map(|entries| entries.count() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let table_videos = schema::table_name("videos");
|
||||||
|
|
||||||
|
let registered_videos: Vec<(String, String, String)> = sqlx::query_as(&format!(
|
||||||
|
"SELECT file_uuid, file_name, status FROM {} WHERE file_path LIKE '%demo%' ORDER BY id",
|
||||||
|
table_videos
|
||||||
|
))
|
||||||
|
.fetch_all(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let registered_videos = registered_videos
|
||||||
|
.into_iter()
|
||||||
|
.map(|(uuid, file_name, status)| RegisteredVideo {
|
||||||
|
uuid,
|
||||||
|
file_name,
|
||||||
|
status,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(SftpgoStatusResponse {
|
||||||
|
username: "demo".to_string(),
|
||||||
|
home_dir: demo_dir.to_string(),
|
||||||
|
files_count,
|
||||||
|
registered_videos,
|
||||||
|
last_login: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct IngestionStep {
|
||||||
|
name: String,
|
||||||
|
status: String,
|
||||||
|
detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct IdentityRef {
|
||||||
|
uuid: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct IngestionStatusResponse {
|
||||||
|
file_uuid: String,
|
||||||
|
steps: Vec<IngestionStep>,
|
||||||
|
related_identities: Vec<IdentityRef>,
|
||||||
|
strangers: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_ingestion_status(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(file_uuid): Path<String>,
|
||||||
|
) -> Result<Json<IngestionStatusResponse>, StatusCode> {
|
||||||
|
let pool = state.db.pool();
|
||||||
|
let chunk = schema::table_name("chunk");
|
||||||
|
let fd = schema::table_name("face_detections");
|
||||||
|
let identities = schema::table_name("identities");
|
||||||
|
|
||||||
|
let scene_meta_path = format!(
|
||||||
|
"{}/{}.scene_meta.json",
|
||||||
|
crate::core::config::OUTPUT_DIR.as_str(),
|
||||||
|
file_uuid
|
||||||
|
);
|
||||||
|
let scene_meta_ok = std::path::Path::new(&scene_meta_path).exists();
|
||||||
|
|
||||||
|
macro_rules! count_sql {
|
||||||
|
($sql:expr) => {
|
||||||
|
sqlx::query_scalar::<_, i64>($sql)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let sentence_count = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'sentence'"
|
||||||
|
));
|
||||||
|
let sentence_embedded = count_sql!(&format!("SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'sentence' AND embedding IS NOT NULL"));
|
||||||
|
let scene_count = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'cut'"
|
||||||
|
));
|
||||||
|
let face_total = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {fd} WHERE file_uuid = '{file_uuid}'"
|
||||||
|
));
|
||||||
|
let trace_count = count_sql!(&format!("SELECT COUNT(DISTINCT trace_id) FROM {fd} WHERE file_uuid = '{file_uuid}' AND trace_id IS NOT NULL"));
|
||||||
|
let trace_chunks = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'trace'"
|
||||||
|
));
|
||||||
|
let identity_count = count_sql!(&format!("SELECT COUNT(DISTINCT identity_id) FROM {fd} WHERE file_uuid = '{file_uuid}' AND identity_id IS NOT NULL"));
|
||||||
|
let tkg_nodes = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} WHERE file_uuid = '{file_uuid}'",
|
||||||
|
schema::table_name("tkg_nodes")
|
||||||
|
));
|
||||||
|
let tkg_edges = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} WHERE file_uuid = '{file_uuid}'",
|
||||||
|
schema::table_name("tkg_edges")
|
||||||
|
));
|
||||||
|
let scene_5w1h = count_sql!(&format!("SELECT COUNT(*) FROM {chunk} WHERE file_uuid = '{file_uuid}' AND chunk_type = 'cut' AND summary_text IS NOT NULL AND summary_text != ''"));
|
||||||
|
|
||||||
|
let related_identities: Vec<IdentityRef> =
|
||||||
|
match sqlx::query_as::<_, (String, String)>(&format!(
|
||||||
|
"SELECT DISTINCT i.uuid::text, i.name FROM {identities} i \
|
||||||
|
JOIN {fd} fd ON fd.identity_id = i.id \
|
||||||
|
WHERE fd.file_uuid = '{file_uuid}' AND fd.identity_id IS NOT NULL \
|
||||||
|
ORDER BY i.name"
|
||||||
|
))
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(rows) => rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(uuid, name)| IdentityRef {
|
||||||
|
uuid: uuid.replace('-', ""),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("related_identities query failed: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let strangers = count_sql!(&format!(
|
||||||
|
"SELECT COUNT(DISTINCT trace_id) FROM {fd} \
|
||||||
|
WHERE file_uuid = '{file_uuid}' AND trace_id IS NOT NULL AND identity_id IS NULL"
|
||||||
|
));
|
||||||
|
|
||||||
|
macro_rules! step {
|
||||||
|
($name:expr, $done:expr, $detail:expr) => {
|
||||||
|
IngestionStep {
|
||||||
|
name: $name.into(),
|
||||||
|
status: if $done { "done" } else { "pending" }.into(),
|
||||||
|
detail: $detail,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let steps = vec![
|
||||||
|
step!(
|
||||||
|
"rule1_sentence",
|
||||||
|
sentence_count > 0,
|
||||||
|
Some(format!("{sentence_count} sentence chunks"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"auto_vectorize",
|
||||||
|
sentence_embedded > 0,
|
||||||
|
Some(format!("{sentence_embedded} embedded"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"rule3_scene",
|
||||||
|
scene_count > 0,
|
||||||
|
Some(format!("{scene_count} scene chunks"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"face_trace",
|
||||||
|
trace_count > 0,
|
||||||
|
Some(format!("{trace_count} traces / {face_total} detections"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"trace_chunks",
|
||||||
|
trace_chunks > 0,
|
||||||
|
Some(format!("{trace_chunks} trace chunks"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"tkg",
|
||||||
|
tkg_nodes > 0 || tkg_edges > 0,
|
||||||
|
Some(format!("{tkg_nodes} nodes, {tkg_edges} edges"))
|
||||||
|
),
|
||||||
|
step!(
|
||||||
|
"identity_match",
|
||||||
|
identity_count > 0,
|
||||||
|
Some(format!("{identity_count} identities matched"))
|
||||||
|
),
|
||||||
|
step!("scene_metadata", scene_meta_ok, None),
|
||||||
|
step!(
|
||||||
|
"5w1h",
|
||||||
|
scene_5w1h > 0,
|
||||||
|
Some(format!("{scene_5w1h} scenes with 5W1H"))
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Ok(Json(IngestionStatusResponse {
|
||||||
|
file_uuid,
|
||||||
|
steps,
|
||||||
|
related_identities,
|
||||||
|
strangers,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/v1/files/scan", get(scan_files))
|
||||||
|
.route("/api/v1/stats/sftpgo", get(get_sftpgo_status))
|
||||||
|
.route(
|
||||||
|
"/api/v1/stats/ingestion-status/:file_uuid",
|
||||||
|
get(get_ingestion_status),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ pub struct SmartSearchResponse {
|
|||||||
// --- API Handler ---
|
// --- API Handler ---
|
||||||
|
|
||||||
pub async fn smart_search(
|
pub async fn smart_search(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<SmartSearchRequest>,
|
Json(req): Json<SmartSearchRequest>,
|
||||||
) -> Result<Json<SmartSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<SmartSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let db = &state.db;
|
let db = &state.db;
|
||||||
@@ -126,6 +126,6 @@ pub async fn smart_search(
|
|||||||
|
|
||||||
// --- Router Setup ---
|
// --- Router Setup ---
|
||||||
|
|
||||||
pub fn search_routes() -> Router<crate::api::server::AppState> {
|
pub fn search_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new().route("/api/v1/search/smart", post(smart_search))
|
Router::new().route("/api/v1/search/smart", post(smart_search))
|
||||||
}
|
}
|
||||||
|
|||||||
4951
src/api/server.rs
4951
src/api/server.rs
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::api::server::AppState;
|
use crate::api::types::AppState;
|
||||||
use crate::core::config;
|
use crate::core::config;
|
||||||
use crate::core::db::{PostgresDb, QdrantDb};
|
use crate::core::db::{PostgresDb, QdrantDb};
|
||||||
use crate::core::tmdb;
|
use crate::core::tmdb;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::core::db::PostgresDb;
|
use crate::core::db::PostgresDb;
|
||||||
|
|
||||||
pub fn trace_agent_routes() -> Router<crate::api::server::AppState> {
|
pub fn trace_agent_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/file/:file_uuid/traces", post(list_traces_sorted))
|
.route("/api/v1/file/:file_uuid/traces", post(list_traces_sorted))
|
||||||
.route(
|
.route(
|
||||||
@@ -55,7 +55,7 @@ struct TracesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_traces_sorted(
|
async fn list_traces_sorted(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
Json(req): Json<TracesRequest>,
|
Json(req): Json<TracesRequest>,
|
||||||
) -> Result<Json<TracesResponse>, (StatusCode, String)> {
|
) -> Result<Json<TracesResponse>, (StatusCode, String)> {
|
||||||
@@ -202,7 +202,7 @@ fn lerp_i32(a: Option<i32>, b: Option<i32>, t: f64) -> Option<i32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_trace_faces(
|
async fn list_trace_faces(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::types::AppState>,
|
||||||
Path((file_uuid, trace_id)): Path<(String, i32)>,
|
Path((file_uuid, trace_id)): Path<(String, i32)>,
|
||||||
Query(q): Query<TraceFacesQuery>,
|
Query(q): Query<TraceFacesQuery>,
|
||||||
) -> Result<Json<TraceFacesResponse>, (StatusCode, String)> {
|
) -> Result<Json<TraceFacesResponse>, (StatusCode, String)> {
|
||||||
|
|||||||
9
src/api/types.rs
Normal file
9
src/api/types.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: std::sync::Arc<crate::core::db::PostgresDb>,
|
||||||
|
pub embedder: std::sync::Arc<crate::Embedder>,
|
||||||
|
pub embedder_model: String,
|
||||||
|
pub mongo_cache: crate::core::cache::MongoCache,
|
||||||
|
pub redis_cache: crate::core::cache::RedisCache,
|
||||||
|
pub api_state: super::middleware::ApiState,
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ pub enum SearchResult {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn universal_search_routes() -> Router<crate::api::server::AppState> {
|
pub fn universal_search_routes() -> Router<crate::api::types::AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/search/universal", post(universal_search))
|
.route("/api/v1/search/universal", post(universal_search))
|
||||||
.route("/api/v1/search/frames", post(search_frames))
|
.route("/api/v1/search/frames", post(search_frames))
|
||||||
@@ -106,7 +106,7 @@ pub fn universal_search_routes() -> Router<crate::api::server::AppState> {
|
|||||||
|
|
||||||
/// Unified search across all data types
|
/// Unified search across all data types
|
||||||
pub async fn universal_search(
|
pub async fn universal_search(
|
||||||
State(_state): State<crate::api::server::AppState>,
|
State(_state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<UniversalSearchRequest>,
|
Json(req): Json<UniversalSearchRequest>,
|
||||||
) -> Result<Json<UniversalSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<UniversalSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
@@ -212,7 +212,7 @@ pub async fn universal_search(
|
|||||||
|
|
||||||
/// Search frames by YOLO objects, OCR text, or face IDs
|
/// Search frames by YOLO objects, OCR text, or face IDs
|
||||||
pub async fn search_frames(
|
pub async fn search_frames(
|
||||||
State(_state): State<crate::api::server::AppState>,
|
State(_state): State<crate::api::types::AppState>,
|
||||||
Json(req): Json<FrameSearchRequest>,
|
Json(req): Json<FrameSearchRequest>,
|
||||||
) -> Result<Json<FrameSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<FrameSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let db = PostgresDb::init().await.map_err(|e| {
|
let db = PostgresDb::init().await.map_err(|e| {
|
||||||
@@ -238,7 +238,7 @@ pub async fn search_frames(
|
|||||||
|
|
||||||
/// Search persons by name or speaker_id
|
/// Search persons by name or speaker_id
|
||||||
pub async fn search_persons(
|
pub async fn search_persons(
|
||||||
State(_state): State<crate::api::server::AppState>,
|
State(_state): State<crate::api::types::AppState>,
|
||||||
Query(query): Query<PersonSearchQuery>,
|
Query(query): Query<PersonSearchQuery>,
|
||||||
) -> Result<Json<PersonSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<PersonSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let db = PostgresDb::init().await.map_err(|e| {
|
let db = PostgresDb::init().await.map_err(|e| {
|
||||||
|
|||||||
217
src/api/visual_search.rs
Normal file
217
src/api/visual_search.rs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use super::types::AppState;
|
||||||
|
use super::visual_chunk_search;
|
||||||
|
use crate::core::cache::keys;
|
||||||
|
use crate::core::chunk::types::Chunk;
|
||||||
|
use crate::core::db::{Database, PostgresDb};
|
||||||
|
|
||||||
|
fn generate_visual_search_hash(
|
||||||
|
uuid: &str,
|
||||||
|
criteria: &visual_chunk_search::VisualChunkSearchCriteria,
|
||||||
|
) -> String {
|
||||||
|
let data = serde_json::json!({
|
||||||
|
"uuid": uuid,
|
||||||
|
"criteria": criteria,
|
||||||
|
});
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(data.to_string().as_bytes());
|
||||||
|
format!("{:x}", hasher.finalize())[..16].to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VisualChunkSearchRequest {
|
||||||
|
file_uuid: String,
|
||||||
|
criteria: visual_chunk_search::VisualChunkSearchCriteria,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct VisualChunkSearchResponse {
|
||||||
|
chunks: Vec<Chunk>,
|
||||||
|
total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_visual_chunks(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<VisualChunkSearchRequest>,
|
||||||
|
) -> Result<Json<VisualChunkSearchResponse>, StatusCode> {
|
||||||
|
let criteria_hash = generate_visual_search_hash(&req.file_uuid, &req.criteria);
|
||||||
|
let cache_key = keys::visual_search(&req.file_uuid, &criteria_hash);
|
||||||
|
let ttl = state.mongo_cache.ttl_visual_search();
|
||||||
|
|
||||||
|
let chunks = state
|
||||||
|
.mongo_cache
|
||||||
|
.get_or_fetch(&cache_key, ttl, keys::CATEGORY_VISUAL_SEARCH, || async {
|
||||||
|
let db = PostgresDb::init()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?;
|
||||||
|
|
||||||
|
visual_chunk_search::search_visual_chunks(&db, &req.file_uuid, &req.criteria)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Visual search failed: {}", e))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Visual chunk search failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(VisualChunkSearchResponse {
|
||||||
|
total: chunks.len(),
|
||||||
|
chunks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VisualChunkSearchByClassRequest {
|
||||||
|
uuid: String,
|
||||||
|
object_class: String,
|
||||||
|
min_count: Option<u32>,
|
||||||
|
max_count: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VisualChunkSearchByDensityRequest {
|
||||||
|
uuid: String,
|
||||||
|
min_density: f32,
|
||||||
|
max_density: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VisualChunkStatsRequest {
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_visual_chunks_by_class(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<VisualChunkSearchByClassRequest>,
|
||||||
|
) -> Result<Json<VisualChunkSearchResponse>, StatusCode> {
|
||||||
|
let db = PostgresDb::init()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let chunks = visual_chunk_search::search_visual_chunks_by_class(
|
||||||
|
&db,
|
||||||
|
&req.uuid,
|
||||||
|
&req.object_class,
|
||||||
|
req.min_count,
|
||||||
|
req.max_count,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Visual chunk search by class failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(VisualChunkSearchResponse {
|
||||||
|
total: chunks.len(),
|
||||||
|
chunks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_visual_chunks_by_density(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<VisualChunkSearchByDensityRequest>,
|
||||||
|
) -> Result<Json<VisualChunkSearchResponse>, StatusCode> {
|
||||||
|
let db = PostgresDb::init()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let chunks = visual_chunk_search::search_visual_chunks_by_density(
|
||||||
|
&db,
|
||||||
|
&req.uuid,
|
||||||
|
req.min_density,
|
||||||
|
req.max_density,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Visual chunk search by density failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(VisualChunkSearchResponse {
|
||||||
|
total: chunks.len(),
|
||||||
|
chunks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct VisualChunkStatsResponse {
|
||||||
|
uuid: String,
|
||||||
|
stats: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_visual_chunk_stats(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<VisualChunkStatsRequest>,
|
||||||
|
) -> Result<Json<VisualChunkStatsResponse>, StatusCode> {
|
||||||
|
let db = PostgresDb::init()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let stats = visual_chunk_search::get_visual_chunk_statistics(&db, &req.uuid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Get visual chunk stats failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(VisualChunkStatsResponse {
|
||||||
|
uuid: req.uuid,
|
||||||
|
stats,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VisualChunkSearchByCombinationRequest {
|
||||||
|
uuid: String,
|
||||||
|
combination: Vec<(String, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_visual_chunks_by_combination(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<VisualChunkSearchByCombinationRequest>,
|
||||||
|
) -> Result<Json<VisualChunkSearchResponse>, StatusCode> {
|
||||||
|
let db = PostgresDb::init()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let combination: Vec<(&str, u32)> = req
|
||||||
|
.combination
|
||||||
|
.iter()
|
||||||
|
.map(|(c, n)| (c.as_str(), *n))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let chunks =
|
||||||
|
visual_chunk_search::search_visual_chunks_by_combination(&db, &req.uuid, &combination)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Visual chunk search by combination failed: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(VisualChunkSearchResponse {
|
||||||
|
total: chunks.len(),
|
||||||
|
chunks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn visual_search_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/v1/search/visual", post(search_visual_chunks))
|
||||||
|
.route(
|
||||||
|
"/api/v1/search/visual/class",
|
||||||
|
post(search_visual_chunks_by_class),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/search/visual/density",
|
||||||
|
post(search_visual_chunks_by_density),
|
||||||
|
)
|
||||||
|
.route("/api/v1/search/visual/stats", post(get_visual_chunk_stats))
|
||||||
|
.route(
|
||||||
|
"/api/v1/search/visual/combination",
|
||||||
|
post(search_visual_chunks_by_combination),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ pub mod types;
|
|||||||
|
|
||||||
pub use rule1_ingest::execute_rule1;
|
pub use rule1_ingest::execute_rule1;
|
||||||
pub use rule3_ingest::ingest_rule3;
|
pub use rule3_ingest::ingest_rule3;
|
||||||
pub use trace_ingest::ingest_traces;
|
|
||||||
pub use splitter::{AsrSegment, ChunkSplitter};
|
pub use splitter::{AsrSegment, ChunkSplitter};
|
||||||
pub use trace_ingest::ingest_traces;
|
pub use trace_ingest::ingest_traces;
|
||||||
pub use types::{Chunk, ChunkType};
|
pub use types::{Chunk, ChunkType};
|
||||||
|
|||||||
Reference in New Issue
Block a user