feat: add source prefix to search results
- 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
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user