87aa7e0c40
- Add [OCR], [ASRX], [ASRX+OCR] prefix to text_content - Add content field to SemanticSearchResult struct - Update SQL queries to include content field - Helps users distinguish the source of search results
392 lines
11 KiB
Rust
392 lines
11 KiB
Rust
use axum::{
|
|
Extension, Json,
|
|
extract::{Query, State},
|
|
http::StatusCode,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::PgPool;
|
|
|
|
use crate::core::db::schema;
|
|
use crate::api::middleware::UserAuth;
|
|
use crate::api::types::AppState;
|
|
|
|
// ─── Trace Profile ───
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TraceProfileQuery {
|
|
pub file_uuid: String,
|
|
pub trace_id: i64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct TraceProfile {
|
|
pub file_uuid: String,
|
|
pub trace_id: i64,
|
|
pub name: String,
|
|
pub key_frame: Option<i64>,
|
|
pub key_face: Option<String>,
|
|
pub aliases: Option<serde_json::Value>,
|
|
pub bbox: Option<serde_json::Value>,
|
|
pub properties: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UpdateTraceProfileRequest {
|
|
pub file_uuid: String,
|
|
pub trace_id: i64,
|
|
pub name: Option<String>,
|
|
pub key_frame: Option<i64>,
|
|
pub key_face: Option<String>,
|
|
pub aliases: Option<serde_json::Value>,
|
|
pub properties: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UpdateTraceProfileGroupRequest {
|
|
pub file_uuid: String,
|
|
pub trace_ids: Vec<i64>,
|
|
pub name: String,
|
|
}
|
|
|
|
pub async fn get_trace_profile_handler(
|
|
State(state): State<AppState>,
|
|
Extension(_auth): Extension<UserAuth>,
|
|
Query(params): Query<TraceProfileQuery>,
|
|
) -> Result<Json<TraceProfile>, StatusCode> {
|
|
let tkg_table = schema::table_name("tkg_nodes");
|
|
let external_id = format!("face_track_{}", params.trace_id);
|
|
|
|
let row: Option<(String, String, serde_json::Value)> = sqlx::query_as(&format!(
|
|
"SELECT label, external_id, properties FROM {} \
|
|
WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2",
|
|
tkg_table
|
|
))
|
|
.bind(¶ms.file_uuid)
|
|
.bind(&external_id)
|
|
.fetch_optional(state.db.pool())
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("[TraceProfile] DB error: {}", e);
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?;
|
|
|
|
let (label, _ext_id, properties) = row.ok_or(StatusCode::NOT_FOUND)?;
|
|
|
|
let key_frame = properties.get("key_frame").and_then(|v| v.as_i64());
|
|
let key_face = properties
|
|
.get("key_face")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
let aliases = properties.get("aliases").cloned();
|
|
let bbox = properties.get("avg_bbox").cloned();
|
|
|
|
Ok(Json(TraceProfile {
|
|
file_uuid: params.file_uuid,
|
|
trace_id: params.trace_id,
|
|
name: label,
|
|
key_frame,
|
|
key_face,
|
|
aliases,
|
|
bbox,
|
|
properties,
|
|
}))
|
|
}
|
|
|
|
pub async fn update_trace_profile_handler(
|
|
State(state): State<AppState>,
|
|
Extension(_auth): Extension<UserAuth>,
|
|
Json(req): Json<UpdateTraceProfileRequest>,
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
let tkg_table = schema::table_name("tkg_nodes");
|
|
let external_id = format!("face_track_{}", req.trace_id);
|
|
|
|
// Get current node
|
|
let current: Option<(String, serde_json::Value)> = sqlx::query_as(&format!(
|
|
"SELECT label, properties FROM {} \
|
|
WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2",
|
|
tkg_table
|
|
))
|
|
.bind(&req.file_uuid)
|
|
.bind(&external_id)
|
|
.fetch_optional(state.db.pool())
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("[TraceProfile] DB error: {}", e);
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?;
|
|
|
|
let (current_label, mut current_props) = current.ok_or(StatusCode::NOT_FOUND)?;
|
|
|
|
// Build updates
|
|
let mut updates: Vec<String> = Vec::new();
|
|
|
|
if let Some(ref new_name) = req.name {
|
|
if new_name != ¤t_label {
|
|
updates.push(format!("label = $3"));
|
|
}
|
|
}
|
|
|
|
// Merge properties
|
|
if let Some(ref new_props) = req.properties {
|
|
if let Some(obj) = new_props.as_object() {
|
|
for (k, v) in obj {
|
|
current_props[k] = v.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ref key_frame) = req.key_frame {
|
|
current_props["key_frame"] = serde_json::json!(key_frame);
|
|
}
|
|
|
|
if let Some(ref key_face) = req.key_face {
|
|
current_props["key_face"] = serde_json::json!(key_face);
|
|
}
|
|
|
|
if let Some(ref aliases) = req.aliases {
|
|
current_props["aliases"] = aliases.clone();
|
|
}
|
|
|
|
if updates.is_empty() && req.properties.is_none() && req.key_frame.is_none()
|
|
&& req.key_face.is_none() && req.aliases.is_none()
|
|
{
|
|
return Ok(Json(serde_json::json!({
|
|
"success": true,
|
|
"message": "No changes"
|
|
})));
|
|
}
|
|
|
|
let mut query = format!(
|
|
"UPDATE {} SET properties = $4",
|
|
tkg_table
|
|
);
|
|
if !updates.is_empty() {
|
|
query.push_str(", ");
|
|
query.push_str(&updates.join(", "));
|
|
}
|
|
query.push_str(" WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2");
|
|
|
|
let param_idx = if updates.is_empty() { 3 } else { 4 };
|
|
query.push_str(&format!(" RETURNING id"));
|
|
|
|
let result = if !updates.is_empty() {
|
|
sqlx::query(&query)
|
|
.bind(&req.file_uuid)
|
|
.bind(&external_id)
|
|
.bind(req.name.as_ref().unwrap_or(¤t_label))
|
|
.bind(¤t_props)
|
|
.execute(state.db.pool())
|
|
.await
|
|
} else {
|
|
sqlx::query(&query)
|
|
.bind(&req.file_uuid)
|
|
.bind(&external_id)
|
|
.bind(¤t_props)
|
|
.execute(state.db.pool())
|
|
.await
|
|
};
|
|
|
|
match result {
|
|
Ok(res) if res.rows_affected() > 0 => Ok(Json(serde_json::json!({
|
|
"success": true,
|
|
"message": "Trace profile updated",
|
|
"file_uuid": req.file_uuid,
|
|
"trace_id": req.trace_id
|
|
}))),
|
|
Ok(_) => Err(StatusCode::NOT_FOUND),
|
|
Err(e) => {
|
|
tracing::error!("[TraceProfile] Update failed: {}", e);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn update_trace_profile_group_handler(
|
|
State(state): State<AppState>,
|
|
Extension(_auth): Extension<UserAuth>,
|
|
Json(req): Json<UpdateTraceProfileGroupRequest>,
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
let tkg_table = schema::table_name("tkg_nodes");
|
|
|
|
let mut updated = 0;
|
|
for trace_id in &req.trace_ids {
|
|
let external_id = format!("face_track_{}", trace_id);
|
|
let result = sqlx::query(&format!(
|
|
"UPDATE {} SET label = $1 \
|
|
WHERE file_uuid = $2 AND node_type = 'face_track' AND external_id = $3",
|
|
tkg_table
|
|
))
|
|
.bind(&req.name)
|
|
.bind(&req.file_uuid)
|
|
.bind(&external_id)
|
|
.execute(state.db.pool())
|
|
.await;
|
|
|
|
if let Ok(res) = result {
|
|
updated += res.rows_affected();
|
|
}
|
|
}
|
|
|
|
Ok(Json(serde_json::json!({
|
|
"success": true,
|
|
"message": format!("Updated {} traces in group", updated),
|
|
"file_uuid": req.file_uuid,
|
|
"updated_count": updated
|
|
})))
|
|
}
|
|
|
|
// ─── File Profile ───
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct FileProfileQuery {
|
|
pub file_uuid: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct FileProfile {
|
|
pub file_uuid: String,
|
|
pub file_name: String,
|
|
pub file_path: String,
|
|
pub status: String,
|
|
pub duration: f64,
|
|
pub width: i32,
|
|
pub height: i32,
|
|
pub fps: f64,
|
|
pub total_frames: i64,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UpdateFileProfileRequest {
|
|
pub file_uuid: String,
|
|
pub file_path: Option<String>,
|
|
pub file_name: Option<String>,
|
|
}
|
|
|
|
pub async fn get_file_profile_handler(
|
|
State(state): State<AppState>,
|
|
Extension(_auth): Extension<UserAuth>,
|
|
Query(params): Query<FileProfileQuery>,
|
|
) -> Result<Json<FileProfile>, StatusCode> {
|
|
let videos_table = schema::table_name("videos");
|
|
|
|
let row: Option<(String, String, String, String, f64, i32, i32, f64, i64)> =
|
|
sqlx::query_as(&format!(
|
|
"SELECT file_uuid, file_name, file_path, status, duration, width, height, fps, \
|
|
COALESCE(total_frames, 0) FROM {} WHERE file_uuid = $1",
|
|
videos_table
|
|
))
|
|
.bind(¶ms.file_uuid)
|
|
.fetch_optional(state.db.pool())
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("[FileProfile] DB error: {}", e);
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?;
|
|
|
|
let (file_uuid, file_name, file_path, status, duration, width, height, fps, total_frames) =
|
|
row.ok_or(StatusCode::NOT_FOUND)?;
|
|
|
|
Ok(Json(FileProfile {
|
|
file_uuid,
|
|
file_name,
|
|
file_path,
|
|
status,
|
|
duration,
|
|
width,
|
|
height,
|
|
fps,
|
|
total_frames,
|
|
}))
|
|
}
|
|
|
|
pub async fn update_file_profile_handler(
|
|
State(state): State<AppState>,
|
|
Extension(_auth): Extension<UserAuth>,
|
|
Json(req): Json<UpdateFileProfileRequest>,
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
let videos_table = schema::table_name("videos");
|
|
|
|
let mut updates: Vec<String> = Vec::new();
|
|
let mut param_idx: i32 = 1;
|
|
|
|
if req.file_path.is_some() {
|
|
updates.push(format!("file_path = ${}", param_idx));
|
|
param_idx += 1;
|
|
}
|
|
if req.file_name.is_some() {
|
|
updates.push(format!("file_name = ${}", param_idx));
|
|
param_idx += 1;
|
|
}
|
|
|
|
if updates.is_empty() {
|
|
return Ok(Json(serde_json::json!({
|
|
"success": true,
|
|
"message": "No changes"
|
|
})));
|
|
}
|
|
|
|
updates.push("updated_at = CURRENT_TIMESTAMP".to_string());
|
|
|
|
let sql = format!(
|
|
"UPDATE {} SET {} WHERE file_uuid = ${}",
|
|
videos_table,
|
|
updates.join(", "),
|
|
param_idx
|
|
);
|
|
|
|
// Build the SQL and execute with correct bind order
|
|
let result = match (&req.file_path, &req.file_name) {
|
|
(Some(fp), Some(fn_)) => {
|
|
sqlx::query(&sql)
|
|
.bind(fp)
|
|
.bind(fn_)
|
|
.bind(&req.file_uuid)
|
|
.execute(state.db.pool())
|
|
.await
|
|
}
|
|
(Some(fp), None) => {
|
|
sqlx::query(&sql)
|
|
.bind(fp)
|
|
.bind(&req.file_uuid)
|
|
.execute(state.db.pool())
|
|
.await
|
|
}
|
|
(None, Some(fn_)) => {
|
|
sqlx::query(&sql)
|
|
.bind(fn_)
|
|
.bind(&req.file_uuid)
|
|
.execute(state.db.pool())
|
|
.await
|
|
}
|
|
(None, None) => unreachable!(),
|
|
};
|
|
|
|
match result {
|
|
Ok(res) if res.rows_affected() > 0 => Ok(Json(serde_json::json!({
|
|
"success": true,
|
|
"message": "File profile updated",
|
|
"file_uuid": req.file_uuid
|
|
}))),
|
|
Ok(_) => Err(StatusCode::NOT_FOUND),
|
|
Err(e) => {
|
|
tracing::error!("[FileProfile] Update failed: {}", e);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Routes ───
|
|
|
|
pub fn profile_routes() -> axum::Router<AppState> {
|
|
use axum::routing::{get, put};
|
|
axum::Router::new()
|
|
.route("/api/v1/trace-profile", get(get_trace_profile_handler))
|
|
.route("/api/v1/trace-profile", put(update_trace_profile_handler))
|
|
.route(
|
|
"/api/v1/trace-profile/group",
|
|
put(update_trace_profile_group_handler),
|
|
)
|
|
.route("/api/v1/file-profile", get(get_file_profile_handler))
|
|
.route("/api/v1/file-profile", put(update_file_profile_handler))
|
|
}
|