feat: ASRX hybrid pipeline, identity history, worker fixes, checkpoint system
This commit is contained in:
+45
-27
@@ -60,13 +60,12 @@ pub struct UniversalSearchResponse {
|
||||
pub enum SearchResult {
|
||||
#[serde(rename = "chunk")]
|
||||
Chunk {
|
||||
file_uuid: String,
|
||||
chunk_id: String,
|
||||
chunk_type: String,
|
||||
// Primary: frame-accurate position
|
||||
start_frame: i64,
|
||||
end_frame: i64,
|
||||
fps: f64,
|
||||
// Reference: time derived from frames (subject to FPS variation)
|
||||
start_time: f64,
|
||||
end_time: f64,
|
||||
score: f64,
|
||||
@@ -76,9 +75,8 @@ pub enum SearchResult {
|
||||
},
|
||||
#[serde(rename = "frame")]
|
||||
Frame {
|
||||
// Primary: exact frame number
|
||||
file_uuid: String,
|
||||
frame_number: i64,
|
||||
// Reference: time derived from frame (subject to FPS variation)
|
||||
timestamp: f64,
|
||||
score: f64,
|
||||
objects: Option<Vec<serde_json::Value>>,
|
||||
@@ -88,6 +86,7 @@ pub enum SearchResult {
|
||||
},
|
||||
#[serde(rename = "person")]
|
||||
Person {
|
||||
file_uuid: Option<String>,
|
||||
identity_id: i32,
|
||||
identity_uuid: String,
|
||||
name: Option<String>,
|
||||
@@ -328,17 +327,15 @@ async fn search_chunks(
|
||||
db: &PostgresDb,
|
||||
req: &UniversalSearchRequest,
|
||||
) -> Result<Vec<SearchResult>, anyhow::Error> {
|
||||
// uuid is required for chunk search - chunk_id is only unique within a video
|
||||
let uuid = match &req.file_uuid {
|
||||
Some(u) => u.replace('\'', "''"),
|
||||
None => return Err(anyhow::anyhow!("file_uuid is required for chunk search")),
|
||||
};
|
||||
|
||||
let chunk_table = schema::table_name("chunk");
|
||||
let mut sql = format!(
|
||||
"SELECT chunk_id, chunk_type, start_time, end_time, (start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, fps, text_content, content FROM {} WHERE file_uuid = '{}'",
|
||||
chunk_table, uuid
|
||||
"SELECT file_uuid, chunk_id, chunk_type, start_time, end_time, (start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, fps, text_content, content FROM {} WHERE 1=1",
|
||||
chunk_table
|
||||
);
|
||||
|
||||
if let Some(uuid) = &req.file_uuid {
|
||||
sql.push_str(&format!(" AND file_uuid = '{}'", uuid.replace('\'', "''")));
|
||||
}
|
||||
if let Some(tr) = &req.time_range {
|
||||
sql.push_str(&format!(
|
||||
" AND start_time >= {} AND end_time <= {}",
|
||||
@@ -422,6 +419,7 @@ async fn search_chunks(
|
||||
sql.push_str(&format!(" LIMIT {}", req.page_size.unwrap_or(20)));
|
||||
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
f64,
|
||||
@@ -437,6 +435,7 @@ async fn search_chunks(
|
||||
.into_iter()
|
||||
.map(
|
||||
|(
|
||||
file_uuid,
|
||||
chunk_id,
|
||||
chunk_type,
|
||||
start_time,
|
||||
@@ -457,7 +456,6 @@ async fn search_chunks(
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
});
|
||||
// Simple scoring: if query matches, score 0.8
|
||||
let score = if !req.query.is_empty()
|
||||
&& text.as_ref().map_or(false, |t| {
|
||||
t.to_lowercase().contains(&req.query.to_lowercase())
|
||||
@@ -468,6 +466,7 @@ async fn search_chunks(
|
||||
};
|
||||
|
||||
SearchResult::Chunk {
|
||||
file_uuid,
|
||||
chunk_id,
|
||||
chunk_type,
|
||||
start_time,
|
||||
@@ -549,7 +548,7 @@ async fn search_frames_internal(
|
||||
|
||||
let results: Vec<SearchResult> = rows
|
||||
.into_iter()
|
||||
.map(|(frame_number, timestamp, yolo, ocr, face, _uuid)| {
|
||||
.map(|(frame_number, timestamp, yolo, ocr, face, file_uuid)| {
|
||||
let objects = yolo.as_ref().and_then(|v| {
|
||||
v.get("objects")
|
||||
.map(|o| o.as_array().cloned().unwrap_or_default())
|
||||
@@ -571,6 +570,7 @@ async fn search_frames_internal(
|
||||
});
|
||||
|
||||
SearchResult::Frame {
|
||||
file_uuid,
|
||||
frame_number,
|
||||
timestamp,
|
||||
score: 0.7,
|
||||
@@ -589,37 +589,54 @@ async fn search_persons_internal(
|
||||
db: &PostgresDb,
|
||||
req: &UniversalSearchRequest,
|
||||
) -> Result<Vec<SearchResult>, anyhow::Error> {
|
||||
let uuid = match &req.file_uuid {
|
||||
Some(u) => u.replace('\'', "''"),
|
||||
None => return Err(anyhow::anyhow!("file_uuid is required for person search")),
|
||||
};
|
||||
|
||||
let id_table = schema::table_name("identities");
|
||||
let fd_table = schema::table_name("face_detections");
|
||||
let mut sql = format!(
|
||||
"SELECT i.id, i.uuid::text, i.name, COUNT(fd.id) AS appearance_count, \
|
||||
MIN(fd.timestamp_secs) AS first_time, MAX(fd.timestamp_secs) AS last_time \
|
||||
FROM {} i JOIN {} fd ON fd.identity_id = i.id \
|
||||
WHERE fd.file_uuid = '{}'",
|
||||
id_table, fd_table, uuid
|
||||
MIN(fd.timestamp_secs) AS first_time, MAX(fd.timestamp_secs) AS last_time, \
|
||||
fd.file_uuid \
|
||||
FROM {} i JOIN {} fd ON fd.identity_id = i.id WHERE 1=1",
|
||||
id_table, fd_table
|
||||
);
|
||||
|
||||
if let Some(uuid) = &req.file_uuid {
|
||||
sql.push_str(&format!(
|
||||
" AND fd.file_uuid = '{}'",
|
||||
uuid.replace('\'', "''")
|
||||
));
|
||||
}
|
||||
|
||||
if !req.query.is_empty() {
|
||||
let q = req.query.replace('\'', "''");
|
||||
sql.push_str(&format!(" AND i.name ILIKE '%{}%'", q));
|
||||
}
|
||||
|
||||
sql.push_str(" GROUP BY i.id, i.uuid, i.name");
|
||||
sql.push_str(" GROUP BY i.id, i.uuid, i.name, fd.file_uuid");
|
||||
sql.push_str(" ORDER BY appearance_count DESC");
|
||||
sql.push_str(&format!(" LIMIT {}", req.page_size.unwrap_or(20)));
|
||||
|
||||
let rows: Vec<(i32, String, Option<String>, i64, Option<f64>, Option<f64>)> =
|
||||
sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
let rows: Vec<(
|
||||
i32,
|
||||
String,
|
||||
Option<String>,
|
||||
i64,
|
||||
Option<f64>,
|
||||
Option<f64>,
|
||||
String,
|
||||
)> = sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
|
||||
let results: Vec<SearchResult> = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(identity_id, identity_uuid, name, appearance_count, first_time, last_time)| {
|
||||
|(
|
||||
identity_id,
|
||||
identity_uuid,
|
||||
name,
|
||||
appearance_count,
|
||||
first_time,
|
||||
last_time,
|
||||
file_uuid,
|
||||
)| {
|
||||
let score = if !req.query.is_empty()
|
||||
&& name.as_ref().map_or(false, |n| {
|
||||
n.to_lowercase().contains(&req.query.to_lowercase())
|
||||
@@ -630,6 +647,7 @@ async fn search_persons_internal(
|
||||
};
|
||||
|
||||
SearchResult::Person {
|
||||
file_uuid: Some(file_uuid),
|
||||
identity_id,
|
||||
identity_uuid,
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user