feat: backup architecture docs, source code, and scripts
This commit is contained in:
936
src/api/face_recognition.rs
Normal file
936
src/api/face_recognition.rs
Normal file
@@ -0,0 +1,936 @@
|
||||
use axum::{
|
||||
extract::{Multipart, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::db::{schema, Database, PostgresDb};
|
||||
use crate::core::processor::face_recognition::{
|
||||
process_face_recognition, register_face, FaceRecognitionResult, FaceRegistrationResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceRecognitionRequest {
|
||||
pub video_uuid: String,
|
||||
pub enable_recognition: Option<bool>,
|
||||
pub enable_tracking: Option<bool>,
|
||||
pub enable_clustering: Option<bool>,
|
||||
pub database_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceRecognitionResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub result: Option<FaceRecognitionResult>,
|
||||
pub processing_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceRegistrationRequest {
|
||||
pub video_uuid: String,
|
||||
pub name: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceRegistrationApiResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub result: Option<FaceRegistrationResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceSearchRequest {
|
||||
pub video_uuid: String,
|
||||
pub embedding: Vec<f32>,
|
||||
pub similarity_threshold: Option<f64>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceSearchResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub results: Vec<FaceSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceSearchResult {
|
||||
pub face_id: String,
|
||||
pub name: Option<String>,
|
||||
pub similarity: f64,
|
||||
pub attributes: Option<serde_json::Value>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceListQuery {
|
||||
pub video_uuid: String,
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub active_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceListResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub faces: Vec<FaceListItem>,
|
||||
pub count: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceListItem {
|
||||
pub face_id: String,
|
||||
pub name: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub is_active: bool,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn face_recognition_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/face/recognize", post(recognize_faces))
|
||||
.route("/api/v1/face/register", post(register_face_api))
|
||||
.route("/api/v1/face/search", post(search_faces))
|
||||
.route("/api/v1/face/list", get(list_faces))
|
||||
.route("/api/v1/face/:face_id", get(get_face_details))
|
||||
.route("/api/v1/face/:face_id", axum::routing::delete(delete_face))
|
||||
.route(
|
||||
"/api/v1/face/results/:video_uuid",
|
||||
get(get_recognition_results),
|
||||
)
|
||||
}
|
||||
|
||||
async fn recognize_faces(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<FaceRecognitionRequest>,
|
||||
) -> Result<Json<FaceRecognitionResponse>, (StatusCode, String)> {
|
||||
let processing_id = Uuid::new_v4().to_string();
|
||||
|
||||
tracing::info!(
|
||||
"[FACE_RECOGNITION] Starting recognition for video: {}, processing_id: {}",
|
||||
request.video_uuid,
|
||||
processing_id
|
||||
);
|
||||
|
||||
// Get video path from database
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let video_record = match db.get_video_by_uuid(&request.video_uuid).await {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Video not found: {}", request.video_uuid),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to fetch video: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let video_path = video_record.file_path;
|
||||
let output_path = format!(
|
||||
"{}/face_recognition_{}.json",
|
||||
crate::core::config::OUTPUT_DIR.as_str(),
|
||||
processing_id
|
||||
);
|
||||
|
||||
// Process face recognition
|
||||
let result = match process_face_recognition(
|
||||
&video_path,
|
||||
&output_path,
|
||||
Some(&processing_id),
|
||||
request.enable_recognition.unwrap_or(true),
|
||||
request.enable_tracking.unwrap_or(true),
|
||||
request.enable_clustering.unwrap_or(true),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Face recognition failed: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Store results in database
|
||||
if let Err(e) = store_recognition_results(&db, &request.video_uuid, &result).await {
|
||||
tracing::warn!("Failed to store recognition results: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(FaceRecognitionResponse {
|
||||
success: true,
|
||||
message: format!("Face recognition completed for {}", request.video_uuid),
|
||||
result: Some(result),
|
||||
processing_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn register_face_api(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<FaceRegistrationApiResponse>, (StatusCode, String)> {
|
||||
let mut image_path: Option<String> = None;
|
||||
let mut name: Option<String> = None;
|
||||
let mut metadata: Option<serde_json::Value> = None;
|
||||
|
||||
// Parse multipart form data
|
||||
while let Some(field) = multipart.next_field().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to parse form data: {}", e),
|
||||
)
|
||||
})? {
|
||||
let field_name = field.name().unwrap_or("").to_string();
|
||||
|
||||
match field_name.as_str() {
|
||||
"image" => {
|
||||
// Save uploaded image
|
||||
let file_name = format!("face_registration_{}.jpg", Uuid::new_v4());
|
||||
let file_path = format!("/tmp/{}", file_name);
|
||||
|
||||
let data = field.bytes().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to read image data: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
tokio::fs::write(&file_path, &data).await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to save image: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
image_path = Some(file_path);
|
||||
}
|
||||
"name" => {
|
||||
let value = field.text().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to read name: {}", e),
|
||||
)
|
||||
})?;
|
||||
name = Some(value);
|
||||
}
|
||||
"metadata" => {
|
||||
let value = field.text().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to read metadata: {}", e),
|
||||
)
|
||||
})?;
|
||||
metadata = Some(serde_json::from_str(&value).map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid JSON metadata: {}", e),
|
||||
)
|
||||
})?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
let image_path =
|
||||
image_path.ok_or((StatusCode::BAD_REQUEST, "Image is required".to_string()))?;
|
||||
|
||||
let name = name.ok_or((StatusCode::BAD_REQUEST, "Name is required".to_string()))?;
|
||||
|
||||
// Register face
|
||||
let result = match register_face(&image_path, &name, metadata.clone()).await {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
// Clean up temporary file
|
||||
let _ = tokio::fs::remove_file(&image_path).await;
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Face registration failed: {}", e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up temporary file
|
||||
let _ = tokio::fs::remove_file(&image_path).await;
|
||||
|
||||
// Store in PostgreSQL face_identities table
|
||||
if result.success && !result.embedding.is_empty() {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
tracing::warn!("[FACE_REGISTRATION] Failed to connect to DB: {}", e);
|
||||
// Return success even if DB write fails (embedding is still in JSON output)
|
||||
return Ok(Json(FaceRegistrationApiResponse {
|
||||
success: result.success,
|
||||
message: format!("{} (Warning: DB write failed: {})", result.message, e),
|
||||
result: Some(result),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert embedding to PostgreSQL vector format
|
||||
let embedding_str = format!(
|
||||
"[{}]",
|
||||
result
|
||||
.embedding
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
// Insert into face_identities
|
||||
let face_identities_table = schema::table_name("face_identities");
|
||||
let attrs_json =
|
||||
serde_json::to_string(&result.attributes).unwrap_or_else(|_| "{}".to_string());
|
||||
// Use public.vector type to work across schemas
|
||||
let vector_type = if schema::SCHEMA_PREFIX.as_str().is_empty() {
|
||||
"vector".to_string()
|
||||
} else {
|
||||
"public.vector".to_string()
|
||||
};
|
||||
let insert_query = format!(
|
||||
r#"
|
||||
INSERT INTO {} (face_id, name, embedding, attributes, metadata, is_active)
|
||||
VALUES ($1, $2, $3::{}, $4::jsonb, $5, TRUE)
|
||||
ON CONFLICT (face_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
embedding = EXCLUDED.embedding,
|
||||
attributes = EXCLUDED.attributes,
|
||||
metadata = COALESCE(EXCLUDED.metadata, {}.metadata),
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
is_active = TRUE
|
||||
"#,
|
||||
face_identities_table, vector_type, face_identities_table
|
||||
);
|
||||
|
||||
match sqlx::query(&insert_query)
|
||||
.bind(&result.face_id)
|
||||
.bind(&name)
|
||||
.bind(&embedding_str)
|
||||
.bind(&attrs_json)
|
||||
.bind(&metadata.unwrap_or(serde_json::json!({})))
|
||||
.execute(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
"[FACE_REGISTRATION] Stored face '{}' (face_id={}) in DB",
|
||||
name,
|
||||
result.face_id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[FACE_REGISTRATION] Failed to store face in DB: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(FaceRegistrationApiResponse {
|
||||
success: result.success,
|
||||
message: result.message.clone(),
|
||||
result: Some(result),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn search_faces(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<FaceSearchRequest>,
|
||||
) -> Result<Json<FaceSearchResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Convert embedding to PostgreSQL vector format
|
||||
let embedding_str = format!(
|
||||
"[{}]",
|
||||
request
|
||||
.embedding
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
let similarity_threshold = request.similarity_threshold.unwrap_or(0.6);
|
||||
let limit = request.limit.unwrap_or(10);
|
||||
|
||||
// Search for similar faces
|
||||
let face_identities_table = schema::table_name("face_identities");
|
||||
let vector_type = if schema::SCHEMA_PREFIX.as_str().is_empty() {
|
||||
"vector".to_string()
|
||||
} else {
|
||||
"public.vector".to_string()
|
||||
};
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
face_id,
|
||||
name,
|
||||
1 - (embedding <=> $1::{}) as similarity,
|
||||
attributes,
|
||||
metadata
|
||||
FROM {}
|
||||
WHERE is_active = TRUE
|
||||
AND embedding IS NOT NULL
|
||||
AND 1 - (embedding <=> $1::{}) >= $2
|
||||
ORDER BY embedding <=> $1::{}
|
||||
LIMIT $3
|
||||
"#,
|
||||
vector_type, face_identities_table, vector_type, vector_type
|
||||
);
|
||||
|
||||
let results: Vec<FaceSearchResult> = match sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
String,
|
||||
Option<String>,
|
||||
f64,
|
||||
Option<serde_json::Value>,
|
||||
Option<serde_json::Value>,
|
||||
),
|
||||
>(query.as_str())
|
||||
.bind(&embedding_str)
|
||||
.bind(similarity_threshold)
|
||||
.bind(limit)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(face_id, name, similarity, attributes, metadata)| FaceSearchResult {
|
||||
face_id,
|
||||
name,
|
||||
similarity,
|
||||
attributes,
|
||||
metadata,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to search faces: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(FaceSearchResponse {
|
||||
success: true,
|
||||
message: format!("Found {} similar faces", results.len()),
|
||||
results,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_faces(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Query(query): Query<FaceListQuery>,
|
||||
) -> Result<Json<FaceListResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
let active_only = query.active_only.unwrap_or(true);
|
||||
|
||||
// Build query
|
||||
let mut where_clause = "WHERE 1=1".to_string();
|
||||
if active_only {
|
||||
where_clause.push_str(" AND is_active = TRUE");
|
||||
}
|
||||
|
||||
let face_identities_table = schema::table_name("face_identities");
|
||||
let count_query = format!(
|
||||
"SELECT COUNT(*) FROM {} {}",
|
||||
face_identities_table, where_clause
|
||||
);
|
||||
let list_query = format!(
|
||||
"SELECT face_id, name, created_at, updated_at, is_active, metadata FROM {} {} ORDER BY created_at DESC LIMIT $1 OFFSET $2",
|
||||
face_identities_table, where_clause
|
||||
);
|
||||
|
||||
// Get total count
|
||||
let total: i64 = match sqlx::query_scalar(&count_query).fetch_one(db.pool()).await {
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to count faces: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Get face list
|
||||
let faces: Vec<FaceListItem> = match sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
String,
|
||||
Option<String>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
bool,
|
||||
Option<serde_json::Value>,
|
||||
),
|
||||
>(&list_query)
|
||||
.bind(page_size as i32)
|
||||
.bind(offset)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(face_id, name, created_at, updated_at, is_active, metadata)| FaceListItem {
|
||||
face_id,
|
||||
name,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
updated_at: updated_at.to_rfc3339(),
|
||||
is_active,
|
||||
metadata,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to list faces: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(FaceListResponse {
|
||||
success: true,
|
||||
message: format!("Found {} faces", total),
|
||||
faces,
|
||||
count: total,
|
||||
page,
|
||||
page_size,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_face_details(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(face_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let face_identities_table = schema::table_name("face_identities");
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
face_id,
|
||||
name,
|
||||
embedding,
|
||||
attributes,
|
||||
metadata,
|
||||
created_at,
|
||||
updated_at,
|
||||
is_active
|
||||
FROM {}
|
||||
WHERE face_id = $1
|
||||
"#,
|
||||
face_identities_table
|
||||
);
|
||||
|
||||
let face: Option<(
|
||||
String,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<serde_json::Value>,
|
||||
Option<serde_json::Value>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
bool,
|
||||
)> = match sqlx::query_as(&query)
|
||||
.bind(&face_id)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(face) => face,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to fetch face details: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match face {
|
||||
Some((
|
||||
face_id,
|
||||
name,
|
||||
embedding,
|
||||
attributes,
|
||||
metadata,
|
||||
created_at,
|
||||
updated_at,
|
||||
is_active,
|
||||
)) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"face_id": face_id,
|
||||
"name": name,
|
||||
"has_embedding": embedding.is_some(),
|
||||
"attributes": attributes,
|
||||
"metadata": metadata,
|
||||
"created_at": created_at.to_rfc3339(),
|
||||
"updated_at": updated_at.to_rfc3339(),
|
||||
"is_active": is_active
|
||||
});
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Face not found: {}", face_id),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_face(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(face_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Soft delete by marking as inactive
|
||||
let face_identities_table = schema::table_name("face_identities");
|
||||
let query = format!(
|
||||
r#"
|
||||
UPDATE {}
|
||||
SET is_active = FALSE, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE face_id = $1 AND is_active = TRUE
|
||||
RETURNING face_id, name
|
||||
"#,
|
||||
face_identities_table
|
||||
);
|
||||
|
||||
let deleted: Option<(String, Option<String>)> = match sqlx::query_as(&query)
|
||||
.bind(&face_id)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to delete face: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match deleted {
|
||||
Some((deleted_id, name)) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("Face '{}' deleted successfully", name.clone().unwrap_or_else(|| deleted_id.clone())),
|
||||
"face_id": deleted_id.clone()
|
||||
});
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Face not found or already deleted: {}", face_id),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_recognition_results(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(video_uuid): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let query = r#"
|
||||
SELECT
|
||||
video_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
recognized_faces,
|
||||
clusters_count,
|
||||
result_data,
|
||||
processing_time_secs,
|
||||
created_at
|
||||
FROM face_recognition_results
|
||||
WHERE video_uuid = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
let result: Option<(
|
||||
String,
|
||||
i64,
|
||||
f64,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
serde_json::Value,
|
||||
Option<f64>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
)> = match sqlx::query_as(query)
|
||||
.bind(&video_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to fetch recognition results: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Some((
|
||||
video_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
recognized_faces,
|
||||
clusters_count,
|
||||
result_data,
|
||||
processing_time_secs,
|
||||
created_at,
|
||||
)) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"video_uuid": video_uuid,
|
||||
"frame_count": frame_count,
|
||||
"fps": fps,
|
||||
"total_faces": total_faces,
|
||||
"recognized_faces": recognized_faces,
|
||||
"clusters_count": clusters_count,
|
||||
"result_data": result_data,
|
||||
"processing_time_secs": processing_time_secs,
|
||||
"created_at": created_at.to_rfc3339()
|
||||
});
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("No recognition results found for video: {}", video_uuid),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn store_recognition_results(
|
||||
db: &PostgresDb,
|
||||
video_uuid: &str,
|
||||
result: &FaceRecognitionResult,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let total_faces = result.frames.iter().map(|f| f.faces.len()).sum::<usize>();
|
||||
let recognized_faces = result
|
||||
.frames
|
||||
.iter()
|
||||
.flat_map(|f| &f.faces)
|
||||
.filter(|face| face.identity.is_some())
|
||||
.count();
|
||||
|
||||
let query = r#"
|
||||
INSERT INTO face_recognition_results (
|
||||
video_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
recognized_faces,
|
||||
clusters_count,
|
||||
result_data
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (video_uuid) DO UPDATE SET
|
||||
frame_count = EXCLUDED.frame_count,
|
||||
fps = EXCLUDED.fps,
|
||||
total_faces = EXCLUDED.total_faces,
|
||||
recognized_faces = EXCLUDED.recognized_faces,
|
||||
clusters_count = EXCLUDED.clusters_count,
|
||||
result_data = EXCLUDED.result_data,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"#;
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(video_uuid)
|
||||
.bind(result.frame_count as i64)
|
||||
.bind(result.fps)
|
||||
.bind(total_faces as i32)
|
||||
.bind(recognized_faces as i32)
|
||||
.bind(result.face_clusters.len() as i32)
|
||||
.bind(serde_json::to_value(result)?)
|
||||
.execute(db.pool())
|
||||
.await?;
|
||||
|
||||
// Store individual face detections
|
||||
for frame in &result.frames {
|
||||
for face in &frame.faces {
|
||||
if let Some(embedding) = &face.embedding {
|
||||
let embedding_str = format!(
|
||||
"[{}]",
|
||||
embedding
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
let insert_query = r#"
|
||||
INSERT INTO face_detections (
|
||||
video_uuid,
|
||||
frame_number,
|
||||
timestamp_secs,
|
||||
face_id,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
confidence,
|
||||
embedding,
|
||||
attributes,
|
||||
identity_confidence,
|
||||
cluster_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::vector, $11, $12, $13)
|
||||
ON CONFLICT (video_uuid, frame_number, x, y, width, height) DO UPDATE SET
|
||||
face_id = EXCLUDED.face_id,
|
||||
confidence = EXCLUDED.confidence,
|
||||
embedding = EXCLUDED.embedding,
|
||||
attributes = EXCLUDED.attributes,
|
||||
identity_confidence = EXCLUDED.identity_confidence,
|
||||
cluster_id = EXCLUDED.cluster_id
|
||||
"#;
|
||||
|
||||
let identity_confidence = face.identity.as_ref().map(|id| id.confidence as f64);
|
||||
let cluster_id = result
|
||||
.face_clusters
|
||||
.iter()
|
||||
.find(|c| {
|
||||
c.face_ids
|
||||
.contains(&face.face_id.clone().unwrap_or_default())
|
||||
})
|
||||
.map(|c| c.cluster_id.clone());
|
||||
|
||||
sqlx::query(insert_query)
|
||||
.bind(video_uuid)
|
||||
.bind(frame.frame as i64)
|
||||
.bind(frame.timestamp)
|
||||
.bind(face.face_id.as_deref())
|
||||
.bind(face.x)
|
||||
.bind(face.y)
|
||||
.bind(face.width)
|
||||
.bind(face.height)
|
||||
.bind(face.confidence as f64)
|
||||
.bind(&embedding_str)
|
||||
.bind(serde_json::to_value(&face.attributes)?)
|
||||
.bind(identity_confidence)
|
||||
.bind(cluster_id)
|
||||
.execute(db.pool())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store face clusters
|
||||
for cluster in &result.face_clusters {
|
||||
let centroid = &cluster.centroid;
|
||||
let centroid_str = format!(
|
||||
"[{}]",
|
||||
centroid
|
||||
.iter()
|
||||
.map(|v: &f32| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
let cluster_query = r#"
|
||||
INSERT INTO face_clusters (
|
||||
cluster_id,
|
||||
video_uuid,
|
||||
centroid,
|
||||
size,
|
||||
representative_face_id,
|
||||
metadata
|
||||
) VALUES ($1, $2, $3::vector, $4, $5, $6)
|
||||
ON CONFLICT (cluster_id) DO UPDATE SET
|
||||
centroid = EXCLUDED.centroid,
|
||||
size = EXCLUDED.size,
|
||||
representative_face_id = EXCLUDED.representative_face_id,
|
||||
metadata = EXCLUDED.metadata
|
||||
"#;
|
||||
|
||||
sqlx::query(cluster_query)
|
||||
.bind(&cluster.cluster_id)
|
||||
.bind(video_uuid)
|
||||
.bind(¢roid_str)
|
||||
.bind(cluster.size as i32)
|
||||
.bind(cluster.representative_face_id.as_deref())
|
||||
.bind(&cluster.metadata)
|
||||
.execute(db.pool())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user