feat: pending identity sorting, face detail modal, video player fps, processor counts

This commit is contained in:
2026-06-24 02:02:33 +08:00
parent 7855983dc1
commit f915aaf794
9 changed files with 1479 additions and 324 deletions

View File

@@ -44,6 +44,9 @@ struct FileInfo {
#[serde(rename = "isRegistered")]
is_registered: bool,
status: String,
registration_time: Option<String>,
#[serde(rename = "ingested")]
ingested: bool,
}
#[derive(Serialize)]
@@ -76,6 +79,7 @@ struct TraceInfo {
first_sec: f64,
last_sec: f64,
avg_confidence: f64,
face_id: Option<String>,
}
#[derive(Serialize)]
@@ -207,6 +211,7 @@ async fn get_files(args: GetFilesArgs) -> Result<Vec<FileInfo>, String> {
let is_reg = f["is_registered"].as_bool().unwrap_or(false);
let file_uuid = f["file_uuid"].as_str()
.unwrap_or_else(|| f["file_path"].as_str().unwrap_or("")).to_string();
let status = f["status"].as_str().unwrap_or("").to_string();
Some(FileInfo {
file_uuid,
file_name: f["file_name"].as_str()?.to_string(),
@@ -214,7 +219,9 @@ async fn get_files(args: GetFilesArgs) -> Result<Vec<FileInfo>, String> {
file_size: f["file_size"].as_i64().unwrap_or(0),
modified_time: f["modified_time"].as_str().unwrap_or("").to_string(),
is_registered: is_reg,
status: f["status"].as_str().unwrap_or("").to_string(),
registration_time: f["registration_time"].as_str().map(|s| s.to_string()),
ingested: status == "completed",
status,
})
})
.collect();
@@ -276,6 +283,43 @@ async fn process_file(file_uuid: String, processors: Vec<String>) -> Result<Proc
})
}
#[derive(Serialize)]
struct IngestResult {
success: bool,
file_uuid: String,
message: String,
}
#[tauri::command(rename_all = "camelCase")]
async fn ingest_file(file_uuid: String) -> Result<IngestResult, String> {
let client = get_client();
let url = format!("{}/api/v1/file/{}/checkin?api_key={}", CORE_API, file_uuid, API_KEY);
let resp = client.post(&url).send().await
.map_err(|e| format!("Ingest (checkin) request failed: {}", e))?;
let json: serde_json::Value = resp.json().await
.map_err(|e| format!("Ingest (checkin) parse failed: {}", e))?;
Ok(IngestResult {
success: json["success"].as_bool().unwrap_or(false),
file_uuid,
message: json["message"].as_str().unwrap_or("Checkin complete").to_string(),
})
}
#[tauri::command(rename_all = "camelCase")]
async fn checkout_file(file_uuid: String) -> Result<IngestResult, String> {
let client = get_client();
let url = format!("{}/api/v1/file/{}/checkout?api_key={}", CORE_API, file_uuid, API_KEY);
let resp = client.post(&url).send().await
.map_err(|e| format!("Checkout request failed: {}", e))?;
let json: serde_json::Value = resp.json().await
.map_err(|e| format!("Checkout parse failed: {}", e))?;
Ok(IngestResult {
success: json["success"].as_bool().unwrap_or(false),
file_uuid,
message: json["message"].as_str().unwrap_or("Checkout complete").to_string(),
})
}
#[derive(Serialize)]
struct UnregisterResult {
success: bool,
@@ -404,77 +448,50 @@ async fn get_faces(uuid: String, per_page: usize) -> Result<Vec<FaceInfo>, Strin
Ok(faces)
}
#[derive(Serialize)]
struct TracesResponse {
total: i64,
traces: Vec<TraceInfo>,
}
#[tauri::command(rename_all = "camelCase")]
async fn get_traces(uuid: String, per_page: usize) -> Result<Vec<TraceInfo>, String> {
let page_size = per_page.min(20);
let mut all_traces: Vec<TraceInfo> = vec![];
let mut page = 1u32;
let max_retries = 2;
async fn get_traces(uuid: String, per_page: usize, page: Option<u32>) -> Result<TracesResponse, String> {
let page_size = per_page.min(100);
let page_num = page.unwrap_or(1);
let t = std::time::Instant::now();
eprintln!("[get_traces] --> {}", uuid);
for _page in 0..10 {
let url = format!("{}/api/v1/identity/{}/traces?api_key={}&page_size={}&page={}", CORE_API, uuid, API_KEY, page_size, page);
let mut response: Option<reqwest::Response> = None;
for attempt in 0..=max_retries {
if attempt > 0 {
eprintln!("[get_traces] retry {} for page {}", attempt, page);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
match get_client().get(&url).send().await {
Ok(r) => { response = Some(r); break; }
Err(e) => {
eprintln!("[get_traces] attempt {} failed on page {}: {}", attempt + 1, page, e);
}
}
}
let resp = match response {
Some(r) => r,
None => {
eprintln!("[get_traces] all retries exhausted on page {} — returning {} traces so far", page, all_traces.len());
break;
}
};
let json: serde_json::Value = match resp.json().await {
Ok(v) => v,
Err(e) => {
eprintln!("[get_traces] parse error on page {}: {} — returning {} traces so far", page, e, all_traces.len());
break;
}
};
let traces: Vec<TraceInfo> = json["traces"]
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|t| {
Some(TraceInfo {
trace_id: t["trace_id"].as_i64().unwrap_or(0) as i32,
file_uuid: t["file_uuid"].as_str()?.to_string(),
frame_count: t["frame_count"].as_i64().unwrap_or(0),
first_frame: t["first_frame"].as_i64().unwrap_or(0),
last_frame: t["last_frame"].as_i64().unwrap_or(0),
first_sec: t["first_sec"].as_f64().unwrap_or(0.0),
last_sec: t["last_sec"].as_f64().unwrap_or(0.0),
avg_confidence: t["avg_confidence"].as_f64().unwrap_or(0.0),
})
eprintln!("[get_traces] --> {} page={} page_size={}", uuid, page_num, page_size);
let url = format!("{}/api/v1/identity/{}/traces?api_key={}&page_size={}&page={}", CORE_API, uuid, API_KEY, page_size, page_num);
let resp = get_client().get(&url).send().await
.map_err(|e| format!("Traces request failed: {}", e))?;
let json: serde_json::Value = resp.json().await
.map_err(|e| format!("Traces parse failed: {}", e))?;
let total = json["total"].as_i64().unwrap_or(0);
let traces: Vec<TraceInfo> = json["traces"]
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|t| {
Some(TraceInfo {
trace_id: t["trace_id"].as_i64().unwrap_or(0) as i32,
file_uuid: t["file_uuid"].as_str()?.to_string(),
frame_count: t["frame_count"].as_i64().unwrap_or(0),
first_frame: t["first_frame"].as_i64().unwrap_or(0),
last_frame: t["last_frame"].as_i64().unwrap_or(0),
first_sec: t["first_sec"].as_f64().unwrap_or(0.0),
last_sec: t["last_sec"].as_f64().unwrap_or(0.0),
avg_confidence: t["avg_confidence"].as_f64().unwrap_or(0.0),
face_id: t["face_id"].as_str().map(String::from),
})
.collect();
let count = traces.len();
all_traces.extend(traces);
if count < page_size {
break;
}
page += 1;
}
eprintln!("[get_traces] <-- {} {} traces {:?}", uuid, all_traces.len(), t.elapsed());
Ok(all_traces)
})
.collect();
eprintln!("[get_traces] <-- {} total={} {} traces {:?}", uuid, total, traces.len(), t.elapsed());
Ok(TracesResponse { total, traces })
}
#[tauri::command(rename_all = "camelCase")]
@@ -733,6 +750,57 @@ async fn delete_identity(uuid: String) -> Result<(), String> {
Ok(())
}
#[derive(Serialize)]
struct CreateIdentityFromFaceResult {
success: bool,
identity_uuid: String,
name: String,
}
#[tauri::command(rename_all = "camelCase")]
async fn create_identity_from_face(name: String, _face_id: Option<String>, _face_row_id: Option<i64>, file_uuid: String) -> Result<CreateIdentityFromFaceResult, String> {
let client = get_client();
// Use POST /api/v1/file/:file_uuid/pending-person to create pending identity
let url = format!("{}/api/v1/file/{}/pending-person?api_key={}", CORE_API, file_uuid, API_KEY);
let body = serde_json::json!({
"name": name
});
let resp = client.post(&url)
.json(&body)
.send().await
.map_err(|e| format!("Create pending person request failed: {}", e))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().await
.map_err(|e| format!("Parse response failed: {}", e))?;
eprintln!("[create_identity_from_face] status={} resp={}", status, resp_json);
if !status.is_success() {
return Err(format!("Create pending person failed: {} - {}", status, resp_json["error"].as_str().unwrap_or("unknown error")));
}
// identity_uuid is inside data object
let data_obj = resp_json.get("data").unwrap_or(&resp_json);
let new_uuid = data_obj.get("identity_uuid")
.and_then(|v| v.as_str())
.or_else(|| resp_json.get("identity_uuid").and_then(|v| v.as_str()))
.or_else(|| resp_json.get("uuid").and_then(|v| v.as_str()))
.unwrap_or("");
if new_uuid.is_empty() {
return Err("Create pending person failed: no uuid returned".to_string());
}
Ok(CreateIdentityFromFaceResult {
success: true,
identity_uuid: new_uuid.to_string(),
name,
})
}
#[tauri::command(rename_all = "camelCase")]
async fn search_identities(query: String, limit: usize) -> Result<Vec<SearchIdentityResult>, String> {
let url = format!("{}/api/v1/identities/search?api_key={}&q={}", CORE_API, API_KEY, query);
@@ -998,6 +1066,8 @@ fn main() {
get_files,
register_file,
process_file,
ingest_file,
checkout_file,
unregister_file,
get_people,
get_faces,
@@ -1012,6 +1082,7 @@ fn main() {
update_identity,
upload_profile_image,
delete_identity,
create_identity_from_face,
search_identities,
get_face_candidates,
merge_identities,