feat: pending identity sorting, face detail modal, video player fps, processor counts
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user