#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
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>> = Mutex::new(None);
static PROFILE_CACHE: Mutex >> = 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,
}
#[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,
#[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,
confidence: f64,
bbox: Option,
}
#[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,
}
#[derive(Serialize)]
struct FileDetail {
file_uuid: String,
file_name: String,
fps: f64,
duration: f64,
}
#[derive(Serialize)]
struct FaceCandidate {
id: i64,
face_id: Option,
file_uuid: String,
frame_number: i64,
confidence: f64,
bbox: Option,
}
#[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,
file_uuid: Option,
trace_id: Option,
start_frame: Option,
end_frame: Option,
start_time: f64,
end_time: f64,
text_content: Option,
}
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, 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 = 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) -> Result {
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, 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 = 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 {
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) -> Result {
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 {
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 {
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) -> Result {
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, 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["metadata"]["starred"].as_bool().unwrap_or(false),
status: 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, 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 = 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,
}
#[tauri::command(rename_all = "camelCase")]
async fn get_traces(uuid: String, per_page: usize, page: Option) -> Result {
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 = 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 {
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 {
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, bbox_y: Option, bbox_w: Option, bbox_h: Option) -> Result {
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 {
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 {
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, metadata_json: String) -> Result {
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 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, _face_row_id: Option, file_uuid: String) -> Result {
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, 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 = 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, 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 = 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, face_row_id: Option, file_uuid: String) -> 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());
}
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, face_row_id: Option, file_uuid: String, _frame_number: Option) -> 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) -> Result {
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) -> Result {
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, page_size: Option) -> Result {
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) -> Result {
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) -> Result {
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, page_size: Option) -> Result {
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 {
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 {
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, target_uuid: Option, page: Option, page_size: Option) -> Result {
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,
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");
}