Fixes: - VideoPlayer.vue: restore allTags, filteredMarks, marksByTag (removed by mistake) - PeopleView.vue: fix frameTime scope in playFaceDetailVideo catch block - Both errors caused white screen on build failure Root cause: 1. visibleTags duplicate declaration (old + new logic) 2. frameTime undefined in catch block (scope issue) Solution: 1. Keep allTags/filteredMarks/marksByTag (needed in template) 2. Keep visibleTags (new logic for processor toggles) 3. Move frameTime calculation to correct scope in catch block
1181 lines
45 KiB
Plaintext
1181 lines
45 KiB
Plaintext
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
use dirs;
|
|
use markbase_core::vfs::VfsBackend;
|
|
|
|
mod proxy;
|
|
mod db;
|
|
|
|
use serde::Serialize;
|
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
use std::sync::Mutex;
|
|
use std::num::NonZeroUsize;
|
|
use lru::LruCache;
|
|
use image::GenericImageView;
|
|
|
|
static THUMB_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
|
static PROFILE_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
|
|
|
fn get_client() -> reqwest::Client {
|
|
reqwest::Client::builder()
|
|
.connect_timeout(std::time::Duration::from_secs(10))
|
|
.timeout(std::time::Duration::from_secs(60))
|
|
.pool_max_idle_per_host(10)
|
|
.build()
|
|
.expect("Failed to build HTTP client")
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct SearchResult {
|
|
file_uuid: String,
|
|
start_time: f64,
|
|
end_time: f64,
|
|
start_frame: i64,
|
|
end_frame: i64,
|
|
summary: String,
|
|
similarity: f64,
|
|
file_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FileInfo {
|
|
file_uuid: String,
|
|
file_name: String,
|
|
file_path: String,
|
|
file_size: i64,
|
|
modified_time: String,
|
|
#[serde(rename = "isRegistered")]
|
|
is_registered: bool,
|
|
status: String,
|
|
registration_time: Option<String>,
|
|
#[serde(rename = "ingested")]
|
|
ingested: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct PersonInfo {
|
|
identity_uuid: String,
|
|
name: String,
|
|
starred: bool,
|
|
status: String,
|
|
metadata: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FaceInfo {
|
|
id: i64,
|
|
file_uuid: String,
|
|
frame_number: i64,
|
|
timestamp_secs: f64,
|
|
face_id: Option<String>,
|
|
confidence: f64,
|
|
bbox: Option<FaceBBox>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct TraceInfo {
|
|
trace_id: i32,
|
|
file_uuid: String,
|
|
frame_count: i64,
|
|
first_frame: i64,
|
|
last_frame: i64,
|
|
first_sec: f64,
|
|
last_sec: f64,
|
|
avg_confidence: f64,
|
|
face_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FileDetail {
|
|
file_uuid: String,
|
|
file_name: String,
|
|
fps: f64,
|
|
duration: f64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FaceCandidate {
|
|
id: i64,
|
|
face_id: Option<String>,
|
|
file_uuid: String,
|
|
frame_number: i64,
|
|
confidence: f64,
|
|
bbox: Option<FaceBBox>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FaceBBox {
|
|
x: f64,
|
|
y: f64,
|
|
width: f64,
|
|
height: f64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct SearchIdentityResult {
|
|
identity_id: i64,
|
|
name: String,
|
|
source: String,
|
|
tmdb_id: Option<i64>,
|
|
file_uuid: Option<String>,
|
|
trace_id: Option<i64>,
|
|
start_frame: Option<i64>,
|
|
end_frame: Option<i64>,
|
|
start_time: f64,
|
|
end_time: f64,
|
|
text_content: Option<String>,
|
|
}
|
|
|
|
const CORE_API: &str = "http://localhost:3002";
|
|
const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69";
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn search_llm_smart(query: String, limit: usize) -> Result<Vec<SearchResult>, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/search/llm-smart?api_key={}", CORE_API, API_KEY);
|
|
|
|
let response = client
|
|
.post(&url)
|
|
.json(&serde_json::json!({"query": query, "limit": limit}))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
let json: serde_json::Value = response
|
|
.json()
|
|
.await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
|
|
let results: Vec<SearchResult> = json["results"]
|
|
.as_array()
|
|
.unwrap_or(&vec![])
|
|
.iter()
|
|
.filter_map(|r| {
|
|
Some(SearchResult {
|
|
file_uuid: r["file_uuid"].as_str()?.to_string(),
|
|
start_time: r["start_time"].as_f64()?,
|
|
end_time: r["end_time"].as_f64()?,
|
|
start_frame: r["start_frame"].as_i64().unwrap_or(0),
|
|
end_frame: r["end_frame"].as_i64().unwrap_or(0),
|
|
summary: r["summary"].as_str()?.to_string(),
|
|
similarity: r["similarity"].as_f64().unwrap_or(0.0),
|
|
file_name: r["file_name"].as_str().map(|s| s.to_string()),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct GetFilesArgs {
|
|
#[serde(rename = "pageSize")]
|
|
page_size: usize,
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn search_agents(query: String, conversation_id: Option<String>) -> Result<serde_json::Value, String> {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(120))
|
|
.build()
|
|
.map_err(|e| format!("Client build failed: {}", e))?;
|
|
let url = format!("{}/api/v1/agents/search?api_key={}", CORE_API, API_KEY);
|
|
let mut body = serde_json::json!({"query": query});
|
|
if let Some(cid) = conversation_id {
|
|
body["conversation_id"] = serde_json::Value::String(cid);
|
|
}
|
|
let response = client
|
|
.post(&url)
|
|
.json(&body)
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Agent search request failed: {}", e))?;
|
|
let json: serde_json::Value = response
|
|
.json()
|
|
.await
|
|
.map_err(|e| format!("Agent search parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_files(args: GetFilesArgs) -> Result<Vec<FileInfo>, String> {
|
|
let page_size = args.page_size;
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/files/scan?api_key={}&page_size={}", CORE_API, API_KEY, page_size);
|
|
|
|
let response = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?;
|
|
|
|
let files: Vec<FileInfo> = json["files"]
|
|
.as_array()
|
|
.unwrap_or(&vec![])
|
|
.iter()
|
|
.filter_map(|f| {
|
|
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(),
|
|
file_path: f["file_path"].as_str().unwrap_or("").to_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,
|
|
registration_time: f["registration_time"].as_str().map(|s| s.to_string()),
|
|
ingested: status == "completed",
|
|
status,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(files)
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct RegisterResult {
|
|
success: bool,
|
|
file_uuid: String,
|
|
file_name: String,
|
|
message: String,
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn register_file(file_path: String) -> Result<RegisterResult, String> {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(120))
|
|
.build()
|
|
.map_err(|e| format!("Client build failed: {}", e))?;
|
|
let url = format!("{}/api/v1/files/register?api_key={}", CORE_API, API_KEY);
|
|
let response = client.post(&url)
|
|
.json(&serde_json::json!({"file_path": file_path}))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Register request failed: {}", e))?;
|
|
let json: serde_json::Value = response.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(RegisterResult {
|
|
success: json["success"].as_bool().unwrap_or(false),
|
|
file_uuid: json["file_uuid"].as_str().unwrap_or("").to_string(),
|
|
file_name: json["file_name"].as_str().unwrap_or("").to_string(),
|
|
message: json["message"].as_str().unwrap_or("").to_string(),
|
|
})
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ProcessResult {
|
|
success: bool,
|
|
file_uuid: String,
|
|
message: String,
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn process_file(file_uuid: String, processors: Vec<String>) -> Result<ProcessResult, String> {
|
|
let url = format!("{}/api/v1/file/{}/process?api_key={}", CORE_API, file_uuid, API_KEY);
|
|
let response = get_client().post(&url)
|
|
.json(&serde_json::json!({"processors": processors}))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Process request failed: {}", e))?;
|
|
let json: serde_json::Value = response.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(ProcessResult {
|
|
success: json["success"].as_bool().unwrap_or(false),
|
|
file_uuid: json["file_uuid"].as_str().unwrap_or("").to_string(),
|
|
message: json["message"].as_str().unwrap_or("").to_string(),
|
|
})
|
|
}
|
|
|
|
#[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,
|
|
file_uuid: String,
|
|
message: String,
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn unregister_file(file_uuid: String, delete_output_files: Option<bool>) -> Result<UnregisterResult, String> {
|
|
let url = format!("{}/api/v1/unregister?api_key={}", CORE_API, API_KEY);
|
|
let mut body = serde_json::json!({"file_uuid": file_uuid});
|
|
if let Some(dof) = delete_output_files {
|
|
body["delete_output_files"] = serde_json::Value::Bool(dof);
|
|
}
|
|
let response = get_client().post(&url)
|
|
.json(&body)
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Unregister request failed: {}", e))?;
|
|
let json: serde_json::Value = response.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(UnregisterResult {
|
|
success: json["success"].as_bool().unwrap_or(false),
|
|
file_uuid: json["file_uuid"].as_str().unwrap_or("").to_string(),
|
|
message: json["message"].as_str().unwrap_or("").to_string(),
|
|
})
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_people(_page: usize, _per_page: usize) -> Result<Vec<PersonInfo>, String> {
|
|
eprintln!("[get_people] called");
|
|
let mut people = Vec::new();
|
|
for page in 1u32..=2 {
|
|
let url = format!("{}/api/v1/identities?api_key={}&page={}&per_page=100", CORE_API, API_KEY, page);
|
|
let resp = match get_client().get(&url).send().await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("[get_people] page {} request failed: {} — continuing", page, e);
|
|
continue;
|
|
}
|
|
};
|
|
let json: serde_json::Value = match resp.json().await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("[get_people] page {} parse error: {} — continuing", page, e);
|
|
continue;
|
|
}
|
|
};
|
|
let identities = match json["identities"].as_array() {
|
|
Some(arr) => arr,
|
|
None => {
|
|
eprintln!("[get_people] page {} has no identities array — stopping", page);
|
|
break;
|
|
}
|
|
};
|
|
if identities.is_empty() {
|
|
eprintln!("[get_people] page {} empty — stopping", page);
|
|
break;
|
|
}
|
|
for i in identities {
|
|
let uuid = match i["identity_uuid"].as_str() {
|
|
Some(u) => u.to_string(),
|
|
None => continue,
|
|
};
|
|
let name = match i["name"].as_str() {
|
|
Some(n) => n.to_string(),
|
|
None => continue,
|
|
};
|
|
people.push(PersonInfo {
|
|
identity_uuid: uuid,
|
|
name,
|
|
starred: i["starred"].as_bool().unwrap_or_else(|| i["metadata"]["starred"].as_bool().unwrap_or(false)),
|
|
status: i["status"].as_str().unwrap_or_else(|| i["metadata"]["status"].as_str().unwrap_or("pending")).to_string(),
|
|
metadata: i["metadata"].clone(),
|
|
});
|
|
}
|
|
}
|
|
eprintln!("[get_people] returning {} people", people.len());
|
|
Ok(people)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_faces(uuid: String, per_page: usize) -> Result<Vec<FaceInfo>, String> {
|
|
let url = format!("{}/api/v1/identity/{}/faces?api_key={}&page_size={}", CORE_API, uuid, API_KEY, per_page);
|
|
let t = std::time::Instant::now();
|
|
eprintln!("[get_faces] --> {}", uuid);
|
|
|
|
let response = match get_client().get(&url).send().await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("[get_faces] <-- {} ERROR {} {:?}", uuid, e, t.elapsed());
|
|
return Ok(vec![]);
|
|
}
|
|
};
|
|
let json: serde_json::Value = match response.json().await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("[get_faces] <-- {} PARSE_ERR {} {:?}", uuid, e, t.elapsed());
|
|
return Ok(vec![]);
|
|
}
|
|
};
|
|
|
|
let faces: Vec<FaceInfo> = json["data"]
|
|
.as_array()
|
|
.unwrap_or(&vec![])
|
|
.iter()
|
|
.filter_map(|f| {
|
|
Some(FaceInfo {
|
|
id: f["id"].as_i64().unwrap_or(0),
|
|
file_uuid: f["file_uuid"].as_str()?.to_string(),
|
|
frame_number: f["frame_number"].as_i64().unwrap_or(0),
|
|
timestamp_secs: f["timestamp_secs"].as_f64().unwrap_or(0.0),
|
|
face_id: f["face_id"].as_str().map(|s| s.to_string()),
|
|
confidence: f["confidence"].as_f64().unwrap_or(0.0),
|
|
bbox: f["bbox"].as_object().map(|o| FaceBBox {
|
|
x: o["x"].as_f64().unwrap_or(0.0),
|
|
y: o["y"].as_f64().unwrap_or(0.0),
|
|
width: o["width"].as_f64().unwrap_or(0.0),
|
|
height: o["height"].as_f64().unwrap_or(0.0),
|
|
}),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
eprintln!("[get_faces] <-- {} {} faces {:?}", uuid, faces.len(), t.elapsed());
|
|
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, 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] --> {} 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();
|
|
|
|
eprintln!("[get_traces] <-- {} total={} {} traces {:?}", uuid, total, traces.len(), t.elapsed());
|
|
Ok(TracesResponse { total, traces })
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_file_info(uuid: String) -> Result<FileDetail, String> {
|
|
let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
let resp = get_client().get(&url).send().await
|
|
.map_err(|e| format!("File info request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("File info parse failed: {}", e))?;
|
|
Ok(FileDetail {
|
|
file_uuid: json["file_uuid"].as_str().unwrap_or("").to_string(),
|
|
file_name: json["file_name"].as_str().unwrap_or("").to_string(),
|
|
fps: json["fps"].as_f64().unwrap_or(24.0),
|
|
duration: json["duration"].as_f64().unwrap_or(0.0),
|
|
})
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_thumbnail(uuid: String, frame: u32) -> Result<String, String> {
|
|
let key = format!("{}:{}", uuid, frame);
|
|
{
|
|
let mut cache = THUMB_CACHE.lock().unwrap();
|
|
if let Some(cache) = cache.as_mut() {
|
|
if let Some(cached) = cache.get(&key) {
|
|
return Ok(cached.clone());
|
|
}
|
|
}
|
|
}
|
|
let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame);
|
|
let bytes = get_client().get(&url).send().await
|
|
.map_err(|e| format!("Thumbnail request failed: {}", e))?
|
|
.bytes().await
|
|
.map_err(|e| format!("Thumbnail read failed: {}", e))?;
|
|
let result = format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes));
|
|
{
|
|
let mut cache = THUMB_CACHE.lock().unwrap();
|
|
let cache = cache.get_or_insert_with(|| LruCache::new(NonZeroUsize::new(500).unwrap()));
|
|
cache.put(key, result.clone());
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_face_thumbnail(uuid: String, frame: u32, bbox_x: Option<f64>, bbox_y: Option<f64>, bbox_w: Option<f64>, bbox_h: Option<f64>) -> Result<String, String> {
|
|
let key = format!("face:{}:{}:{:?}:{:?}:{:?}:{:?}", uuid, frame, bbox_x, bbox_y, bbox_w, bbox_h);
|
|
{
|
|
let mut cache = THUMB_CACHE.lock().unwrap();
|
|
if let Some(cache) = cache.as_mut() {
|
|
if let Some(cached) = cache.get(&key) {
|
|
return Ok(cached.clone());
|
|
}
|
|
}
|
|
}
|
|
let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame);
|
|
let bytes = get_client().get(&url).send().await
|
|
.map_err(|e| format!("Thumbnail request failed: {}", e))?
|
|
.bytes().await
|
|
.map_err(|e| format!("Thumbnail read failed: {}", e))?;
|
|
|
|
let result = if let (Some(bx), Some(by), Some(bw), Some(bh)) = (bbox_x, bbox_y, bbox_w, bbox_h) {
|
|
let img = image::load_from_memory(&bytes).map_err(|e| format!("Image decode failed: {}", e))?;
|
|
let (w, h) = img.dimensions();
|
|
let (px, py, pw, ph) = if bx <= 1.0 && by <= 1.0 && bw <= 1.0 && bh <= 1.0 {
|
|
((bx * w as f64) as u32, (by * h as f64) as u32, (bw * w as f64) as u32, (bh * h as f64) as u32)
|
|
} else {
|
|
(bx as u32, by as u32, bw as u32, bh as u32)
|
|
};
|
|
let cx = px.min(w.saturating_sub(1));
|
|
let cy = py.min(h.saturating_sub(1));
|
|
let cw = pw.min(w.saturating_sub(cx)).max(1);
|
|
let ch = ph.min(h.saturating_sub(cy)).max(1);
|
|
let cropped = img.crop_imm(cx, cy, cw, ch);
|
|
let mut buf = std::io::Cursor::new(Vec::new());
|
|
cropped.write_to(&mut buf, image::ImageFormat::Jpeg).map_err(|e| format!("Encode failed: {}", e))?;
|
|
format!("data:image/jpeg;base64,{}", STANDARD.encode(buf.into_inner()))
|
|
} else {
|
|
format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))
|
|
};
|
|
|
|
{
|
|
let mut cache = THUMB_CACHE.lock().unwrap();
|
|
let cache = cache.get_or_insert_with(|| LruCache::new(NonZeroUsize::new(500).unwrap()));
|
|
cache.put(key, result.clone());
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
const PROFILE_DIRS: &[&str] = &[
|
|
"/Users/accusys/momentry/output/identities",
|
|
"/Users/accusys/momentry/output_dev/identities",
|
|
"/Volumes/external/momentry/output/identities",
|
|
"/Volumes/external/momentry/output_dev/identities",
|
|
];
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_identity_profile(uuid: String) -> Result<String, String> {
|
|
let uuid_display = uuid.clone();
|
|
let no_dash = uuid.replace('-', "");
|
|
{
|
|
let mut cache = PROFILE_CACHE.lock().unwrap();
|
|
if let Some(cache) = cache.as_mut() {
|
|
if let Some(cached) = cache.get(&uuid_display) {
|
|
eprintln!("[get_identity_profile] <-- {} (cached)", uuid_display);
|
|
return Ok(cached.clone());
|
|
}
|
|
}
|
|
}
|
|
let t = std::time::Instant::now();
|
|
let result = (|| -> Option<String> {
|
|
for dir in PROFILE_DIRS {
|
|
for candidate in [&no_dash, &uuid_display] {
|
|
let path = format!("{}/{}/profile.jpg", dir, candidate);
|
|
if std::path::Path::new(&path).exists() {
|
|
let bytes = std::fs::read(&path).ok()?;
|
|
return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes)));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
})();
|
|
match result {
|
|
Some(data) => {
|
|
let mut cache = PROFILE_CACHE.lock().unwrap();
|
|
let cache = cache.get_or_insert_with(|| LruCache::new(NonZeroUsize::new(500).unwrap()));
|
|
cache.put(uuid_display.clone(), data.clone());
|
|
eprintln!("[get_identity_profile] <-- {} {} {}ms", uuid_display, "FOUND", t.elapsed().as_millis());
|
|
Ok(data)
|
|
}
|
|
None => {
|
|
eprintln!("[get_identity_profile] <-- {} {} {}ms", uuid_display, "404", t.elapsed().as_millis());
|
|
Err("Profile image not found".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn update_identity_name(uuid: String, name: String) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
let resp = client.patch(&url)
|
|
.json(&serde_json::json!({"name": name}))
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Update failed: {}", resp.status()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn update_identity_status(uuid: String, status: String) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
// GET current identity to merge metadata (preserve starred, role, notes, etc.)
|
|
let get_resp = client.get(&url).send().await
|
|
.map_err(|e| format!("GET failed: {}", e))?;
|
|
let json: serde_json::Value = get_resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
let mut metadata = json["metadata"].as_object()
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
metadata.insert("status".to_string(), serde_json::Value::String(status));
|
|
let resp = client.patch(&url)
|
|
.json(&serde_json::json!({"metadata": metadata}))
|
|
.send().await
|
|
.map_err(|e| format!("PATCH failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Update failed: {}", resp.status()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn update_identity_starred(uuid: String, starred: bool) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
let get_resp = client.get(&url).send().await
|
|
.map_err(|e| format!("GET failed: {}", e))?;
|
|
let json: serde_json::Value = get_resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
let mut metadata = json["metadata"].as_object()
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
metadata.insert("starred".to_string(), serde_json::Value::Bool(starred));
|
|
let resp = client.patch(&url)
|
|
.json(&serde_json::json!({"metadata": metadata}))
|
|
.send().await
|
|
.map_err(|e| format!("PATCH failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Update failed: {}", resp.status()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn update_identity(uuid: String, name: Option<String>, metadata_json: String) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({});
|
|
if let Some(n) = &name {
|
|
body["name"] = serde_json::Value::String(n.clone());
|
|
}
|
|
if !metadata_json.is_empty() {
|
|
let meta: serde_json::Value = serde_json::from_str(&metadata_json).map_err(|e| format!("Invalid metadata JSON: {}", e))?;
|
|
body["metadata"] = meta;
|
|
}
|
|
let resp = client.patch(&url)
|
|
.json(&body)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn upload_profile_image(uuid: String, file_path: String) -> Result<(), String> {
|
|
let file_bytes = std::fs::read(&file_path).map_err(|e| format!("Failed to read file: {}", e))?;
|
|
let file_name = std::path::Path::new(&file_path)
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("profile.jpg")
|
|
.to_string();
|
|
let part = reqwest::multipart::Part::bytes(file_bytes)
|
|
.file_name(file_name)
|
|
.mime_str("image/jpeg")
|
|
.unwrap_or_else(|_| reqwest::multipart::Part::bytes(std::fs::read(&file_path).unwrap_or_default()).file_name("profile.jpg"));
|
|
let form = reqwest::multipart::Form::new()
|
|
.part("image", part);
|
|
let url = format!("{}/api/v1/identity/{}/profile-image?api_key={}", CORE_API, uuid, API_KEY);
|
|
let resp = get_client().post(&url)
|
|
.multipart(form)
|
|
.send().await
|
|
.map_err(|e| format!("Upload failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let body = resp.text().await.unwrap_or_default();
|
|
return Err(format!("Upload failed: {} {}", status, body));
|
|
}
|
|
let mut cache = PROFILE_CACHE.lock().unwrap();
|
|
if let Some(c) = cache.as_mut() {
|
|
c.pop(&uuid);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn match_from_photo(identity_uuid: String, file_uuid: String, image_path: Option<String>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/agents/identity/match-from-photo?api_key={}", CORE_API, API_KEY);
|
|
let mut form = reqwest::multipart::Form::new()
|
|
.text("identity_uuid", identity_uuid.replace('-', ""))
|
|
.text("file_uuid", file_uuid);
|
|
if let Some(path) = image_path {
|
|
let file_bytes = std::fs::read(&path).map_err(|e| format!("Failed to read image: {}", e))?;
|
|
let part = reqwest::multipart::Part::bytes(file_bytes)
|
|
.file_name("profile.jpg")
|
|
.mime_str("image/jpeg")
|
|
.unwrap_or_else(|_| reqwest::multipart::Part::bytes(std::fs::read(&path).unwrap_or_default()).file_name("profile.jpg"));
|
|
form = form.part("image", part);
|
|
}
|
|
let resp = client.post(&url)
|
|
.multipart(form)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let body = resp.text().await.unwrap_or_default();
|
|
return Err(format!("Match failed: {} {}", status, body));
|
|
}
|
|
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn set_profile_from_face(uuid: String, file_uuid: String, face_id: Option<String>, id: Option<i64>, trace_id: Option<i32>, frame_number: Option<i64>) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/profile-image/from-face?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({"file_uuid": file_uuid});
|
|
if let Some(fid) = face_id {
|
|
body["face_id"] = serde_json::json!(fid);
|
|
} else if let Some(rid) = id {
|
|
body["id"] = serde_json::json!(rid);
|
|
} else if let Some(tid) = trace_id {
|
|
body["trace_id"] = serde_json::json!(tid);
|
|
if let Some(frame) = frame_number {
|
|
body["frame_number"] = serde_json::json!(frame);
|
|
}
|
|
} else {
|
|
return Err("Either face_id, id, or trace_id is required".to_string());
|
|
}
|
|
let resp = client.post(&url)
|
|
.json(&body)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let body_text = resp.text().await.unwrap_or_default();
|
|
return Err(format!("Set profile failed: {} {}", status, body_text));
|
|
}
|
|
let mut cache = PROFILE_CACHE.lock().unwrap();
|
|
if let Some(c) = cache.as_mut() {
|
|
c.pop(&uuid);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn delete_identity(uuid: String) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY);
|
|
let resp = client.delete(&url)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() && resp.status() != 204 {
|
|
return Err(format!("Delete failed: {}", resp.status()));
|
|
}
|
|
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);
|
|
let resp = get_client().get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?;
|
|
let results: Vec<SearchIdentityResult> = json["results"]
|
|
.as_array()
|
|
.unwrap_or(&vec![])
|
|
.iter()
|
|
.take(limit)
|
|
.filter_map(|r| {
|
|
Some(SearchIdentityResult {
|
|
identity_id: r["identity_id"].as_i64().unwrap_or(0),
|
|
name: r["name"].as_str()?.to_string(),
|
|
source: r["source"].as_str().unwrap_or("").to_string(),
|
|
tmdb_id: r["tmdb_id"].as_i64(),
|
|
file_uuid: r["file_uuid"].as_str().map(|s| s.to_string()),
|
|
trace_id: r["trace_id"].as_i64(),
|
|
start_frame: r["start_frame"].as_i64(),
|
|
end_frame: r["end_frame"].as_i64(),
|
|
start_time: r["start_time"].as_f64().unwrap_or(0.0),
|
|
end_time: r["end_time"].as_f64().unwrap_or(0.0),
|
|
text_content: r["text_content"].as_str().map(|s| s.to_string()),
|
|
})
|
|
})
|
|
.collect();
|
|
Ok(results)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_face_candidates(page: usize, per_page: usize) -> Result<Vec<FaceCandidate>, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/faces/candidates?api_key={}&page={}&page_size={}", CORE_API, API_KEY, page, per_page);
|
|
let resp = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?;
|
|
let candidates: Vec<FaceCandidate> = json["candidates"]
|
|
.as_array()
|
|
.unwrap_or(&vec![])
|
|
.iter()
|
|
.filter_map(|c| {
|
|
Some(FaceCandidate {
|
|
id: c["id"].as_i64().unwrap_or(0),
|
|
face_id: c["face_id"].as_str().map(|s| s.to_string()),
|
|
file_uuid: c["file_uuid"].as_str()?.to_string(),
|
|
frame_number: c["frame_number"].as_i64().unwrap_or(0),
|
|
confidence: c["confidence"].as_f64().unwrap_or(0.0),
|
|
bbox: c["bbox"].as_object().map(|o| FaceBBox {
|
|
x: o["x"].as_f64().unwrap_or(0.0),
|
|
y: o["y"].as_f64().unwrap_or(0.0),
|
|
width: o["width"].as_f64().unwrap_or(0.0),
|
|
height: o["height"].as_f64().unwrap_or(0.0),
|
|
}),
|
|
})
|
|
})
|
|
.collect();
|
|
Ok(candidates)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn merge_identities(uuid: String, into_uuid: String) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/mergeinto?api_key={}", CORE_API, uuid, API_KEY);
|
|
let resp = client.post(&url)
|
|
.json(&serde_json::json!({"into_uuid": into_uuid}))
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Merge failed: {}", resp.status()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn bind_face(uuid: String, face_id: Option<String>, face_row_id: Option<i64>, file_uuid: String, expand_to_trace: Option<bool>) -> Result<(), String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/bind?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({"file_uuid": file_uuid});
|
|
if let Some(fid) = &face_id {
|
|
body["face_id"] = serde_json::json!(fid);
|
|
} else if let Some(rid) = face_row_id {
|
|
body["id"] = serde_json::json!(rid);
|
|
} else {
|
|
return Err("Either face_id or face_row_id is required".to_string());
|
|
}
|
|
if let Some(expand) = expand_to_trace {
|
|
body["expand_to_trace"] = serde_json::json!(expand);
|
|
}
|
|
let resp = client.post(&url)
|
|
.json(&body)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Bind failed: {}", resp.status()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn unbind_face(uuid: String, face_id: Option<String>, face_row_id: Option<i64>, file_uuid: String, _frame_number: Option<i64>) -> Result<(), String> {
|
|
let client = get_client();
|
|
let mut body = serde_json::json!({"file_uuid": file_uuid});
|
|
if let Some(fid) = face_id {
|
|
body["face_id"] = serde_json::json!(fid);
|
|
} else if let Some(rid) = face_row_id {
|
|
body["id"] = serde_json::json!(rid);
|
|
} else {
|
|
return Err("Either face_id or face_row_id is required".to_string());
|
|
}
|
|
let url = format!("{}/api/v1/identity/{}/unbind?api_key={}", CORE_API, uuid, API_KEY);
|
|
eprintln!("[unbind_face] POST {} body={}", url, body);
|
|
let resp = client.post(&url)
|
|
.json(&body)
|
|
.send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
let status = resp.status();
|
|
let resp_text = resp.text().await.unwrap_or_default();
|
|
eprintln!("[unbind_face] status={} body={}", status, resp_text);
|
|
if !status.is_success() {
|
|
return Err(format!("Unbind failed: {} - {}", status, resp_text));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_undo(uuid: String, steps: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/undo?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({});
|
|
if let Some(s) = steps {
|
|
body["steps"] = serde_json::Value::Number(s.into());
|
|
}
|
|
let resp = client.post(&url).json(&body).send().await
|
|
.map_err(|e| format!("Undo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_redo(uuid: String, steps: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/redo?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({});
|
|
if let Some(s) = steps {
|
|
body["steps"] = serde_json::Value::Number(s.into());
|
|
}
|
|
let resp = client.post(&url).json(&body).send().await
|
|
.map_err(|e| format!("Redo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_history(uuid: String, page: Option<usize>, page_size: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let mut url = format!("{}/api/v1/identity/{}/history?api_key={}", CORE_API, uuid, API_KEY);
|
|
if let Some(p) = page { url = format!("{}&page={}", url, p); }
|
|
if let Some(ps) = page_size { url = format!("{}&page_size={}", url, ps); }
|
|
let resp = client.get(&url).send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_bind_undo(uuid: String, steps: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/bind/undo?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({});
|
|
if let Some(s) = steps {
|
|
body["steps"] = serde_json::Value::Number(s.into());
|
|
}
|
|
let resp = client.post(&url).json(&body).send().await
|
|
.map_err(|e| format!("Bind undo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_bind_redo(uuid: String, steps: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/{}/bind/redo?api_key={}", CORE_API, uuid, API_KEY);
|
|
let mut body = serde_json::json!({});
|
|
if let Some(s) = steps {
|
|
body["steps"] = serde_json::Value::Number(s.into());
|
|
}
|
|
let resp = client.post(&url).json(&body).send().await
|
|
.map_err(|e| format!("Bind redo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn identity_bind_history(uuid: String, page: Option<usize>, page_size: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let mut url = format!("{}/api/v1/identity/{}/bind/history?api_key={}", CORE_API, uuid, API_KEY);
|
|
if let Some(p) = page { url = format!("{}&page={}", url, p); }
|
|
if let Some(ps) = page_size { url = format!("{}&page_size={}", url, ps); }
|
|
let resp = client.get(&url).send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn merge_undo(merge_id: String) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/merge/{}/undo?api_key={}", CORE_API, merge_id, API_KEY);
|
|
let resp = client.post(&url).send().await
|
|
.map_err(|e| format!("Merge undo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn merge_redo(merge_id: String) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let url = format!("{}/api/v1/identity/merge/{}/redo?api_key={}", CORE_API, merge_id, API_KEY);
|
|
let resp = client.post(&url).send().await
|
|
.map_err(|e| format!("Merge redo failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn merge_history(source_uuid: Option<String>, target_uuid: Option<String>, page: Option<usize>, page_size: Option<usize>) -> Result<serde_json::Value, String> {
|
|
let client = get_client();
|
|
let mut url = format!("{}/api/v1/identity/merge/history?api_key={}", CORE_API, API_KEY);
|
|
if let Some(s) = source_uuid { url = format!("{}&source_uuid={}", url, s); }
|
|
if let Some(t) = target_uuid { url = format!("{}&target_uuid={}", url, t); }
|
|
if let Some(p) = page { url = format!("{}&page={}", url, p); }
|
|
if let Some(ps) = page_size { url = format!("{}&page_size={}", url, ps); }
|
|
let resp = client.get(&url).send().await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
let json: serde_json::Value = resp.json().await
|
|
.map_err(|e| format!("Parse failed: {}", e))?;
|
|
Ok(json)
|
|
}
|
|
|
|
fn main() {
|
|
std::thread::spawn(|| {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
rt.block_on(proxy::start_proxy_server());
|
|
});
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_shell::init())
|
|
.setup(|app| {
|
|
use tauri::Manager;
|
|
let _ = app.get_webview_window("main");
|
|
if let Err(e) = db::init_db() {
|
|
eprintln!("Failed to init database: {}", e);
|
|
}
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
search_llm_smart,
|
|
search_agents,
|
|
get_files,
|
|
register_file,
|
|
process_file,
|
|
ingest_file,
|
|
checkout_file,
|
|
unregister_file,
|
|
get_people,
|
|
get_faces,
|
|
get_traces,
|
|
get_file_info,
|
|
get_thumbnail,
|
|
get_face_thumbnail,
|
|
get_identity_profile,
|
|
update_identity_name,
|
|
update_identity_status,
|
|
update_identity_starred,
|
|
update_identity,
|
|
upload_profile_image,
|
|
set_profile_from_face,
|
|
match_from_photo,
|
|
delete_identity,
|
|
create_identity_from_face,
|
|
search_identities,
|
|
get_face_candidates,
|
|
merge_identities,
|
|
bind_face,
|
|
unbind_face,
|
|
identity_undo,
|
|
identity_redo,
|
|
identity_history,
|
|
identity_bind_undo,
|
|
identity_bind_redo,
|
|
identity_bind_history,
|
|
merge_undo,
|
|
merge_redo,
|
|
merge_history,
|
|
db::get_search_history,
|
|
db::save_search_history,
|
|
db::rename_search_history,
|
|
db::pin_search_history,
|
|
db::delete_search_history,
|
|
db::get_bookmarks,
|
|
db::save_bookmark,
|
|
db::delete_bookmark
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|