Files
momentry_core/src/api/face_recognition.rs
Warren e75c4d6f07 cleanup: remove dead code and duplicate docs
- Remove session-ses_2f27.md (161KB raw session log)
- Remove 49 ROOT_* duplicate files across REFERENCE/
- Remove 14 duplicate files between REFERENCE/ root and history/
- Remove asr_legacy.rs (dead code, replaced by asr.rs)
- Remove src/core/worker/ (duplicate JobWorker)
- Remove src/core/layers/ (empty directory)
- Remove 4 .bak files in src/
- Remove 7 dead private methods in worker/processor.rs
- Remove backup directory from git tracking
2026-05-04 01:31:21 +08:00

945 lines
28 KiB
Rust

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 file_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 file_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 file_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 file_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/files/:file_uuid/faces/:face_id",
get(get_face_details),
)
.route(
"/api/v1/files/:file_uuid/faces/:face_id",
axum::routing::delete(delete_face),
)
.route(
"/api/v1/face/results/:file_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.file_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.file_uuid).await {
Ok(Some(record)) => record,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
format!("Video not found: {}", request.file_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.file_uuid, &result).await {
tracing::warn!("Failed to store recognition results: {}", e);
}
Ok(Json(FaceRecognitionResponse {
success: true,
message: format!("Face recognition completed for {}", request.file_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(serde_json::to_string(&metadata.unwrap_or(serde_json::json!({}))).unwrap())
.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((file_uuid, face_id)): Path<(String, 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 AND file_uuid = $2
"#,
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)
.bind(&file_uuid)
.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((file_uuid, face_id)): Path<(String, 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 file_uuid = $2 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)
.bind(&file_uuid)
.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(file_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
file_uuid,
frame_count,
fps,
total_faces,
recognized_faces,
clusters_count,
result_data,
processing_time_secs,
created_at
FROM face_recognition_results
WHERE file_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(&file_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((
file_uuid,
frame_count,
fps,
total_faces,
recognized_faces,
clusters_count,
result_data,
processing_time_secs,
created_at,
)) => {
let response = serde_json::json!({
"success": true,
"file_uuid": file_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: {}", file_uuid),
)),
}
}
async fn store_recognition_results(
db: &PostgresDb,
file_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 (
file_uuid,
frame_count,
fps,
total_faces,
recognized_faces,
clusters_count,
result_data
) VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (file_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(file_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 (
file_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 (file_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(file_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,
file_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(file_uuid)
.bind(&centroid_str)
.bind(cluster.size as i32)
.bind(cluster.representative_face_id.as_deref())
.bind(&cluster.metadata)
.execute(db.pool())
.await?;
}
Ok(())
}