#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use serde::Serialize; use base64::{Engine as _, engine::general_purpose::STANDARD}; #[derive(Serialize)] struct SearchResult { file_uuid: String, start_time: f64, end_time: f64, summary: String, similarity: f64, file_name: Option, } #[derive(Serialize)] struct FileInfo { file_uuid: String, file_name: String, file_size: i64, modified_time: String, #[serde(rename = "isRegistered")] is_registered: bool, } #[derive(Serialize)] struct PersonInfo { identity_uuid: String, name: String, starred: bool, } #[derive(Serialize)] struct FaceInfo { id: i64, file_uuid: String, frame_number: i64, timestamp_secs: f64, face_id: Option, confidence: f64, } #[derive(Serialize)] struct TraceInfo { trace_id: i32, file_uuid: String, first_sec: f64, last_sec: f64, avg_confidence: f64, frame_count: i64, } #[derive(Serialize)] struct FaceCandidate { id: i64, face_id: Option, file_uuid: String, frame_number: i64, confidence: f64, } #[derive(Serialize)] struct SearchIdentityResult { identity_id: i64, name: String, source: String, tmdb_id: Option, file_uuid: 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 = reqwest::Client::new(); 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()?, 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 get_files(args: GetFilesArgs) -> Result, String> { let page_size = args.page_size; let client = reqwest::Client::new(); 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(); Some(FileInfo { file_uuid, file_name: f["file_name"].as_str()?.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, }) }) .collect(); Ok(files) } #[tauri::command(rename_all = "camelCase")] async fn get_people(_page: usize, _per_page: usize) -> Result, String> { let client = reqwest::Client::new(); let mut all_people = Vec::new(); let mut page = 1; loop { let url = format!("{}/api/v1/identities?api_key={}&page={}&per_page=100", CORE_API, API_KEY, page); 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 identities = json["identities"].as_array(); let identities = identities.as_ref().map(|v| v.as_slice()).unwrap_or(&[]); if identities.is_empty() { break; } for i in identities { if let (Some(uuid), Some(name)) = (i["identity_uuid"].as_str(), i["name"].as_str()) { all_people.push(PersonInfo { identity_uuid: uuid.to_string(), name: name.to_string(), starred: i["metadata"]["starred"].as_bool().unwrap_or(false), }); } } eprintln!("[get_people] page {} got {} identities, total so far: {}", page, identities.len(), all_people.len()); // API max is 20 per page, keep fetching until we get fewer than 20 if identities.len() < 20 { break; } page += 1; } eprintln!("[get_people] returning {} people from {} pages", all_people.len(), page); Ok(all_people) } #[tauri::command(rename_all = "camelCase")] async fn get_faces(uuid: String, per_page: usize) -> Result, String> { let client = reqwest::Client::new(); let url = format!("{}/api/v1/identity/{}/faces?api_key={}&page_size={}", CORE_API, uuid, API_KEY, per_page); 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 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), }) }) .collect(); Ok(faces) } #[tauri::command(rename_all = "camelCase")] async fn get_traces(uuid: String, per_page: usize) -> Result, String> { let client = reqwest::Client::new(); let url = format!("{}/api/v1/identity/{}/traces?api_key={}&page_size={}", CORE_API, uuid, API_KEY, per_page); 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 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(), 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), frame_count: t["frame_count"].as_i64().unwrap_or(0), }) }) .collect(); Ok(traces) } #[tauri::command(rename_all = "camelCase")] async fn get_thumbnail(uuid: String, frame: u32) -> Result { let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame); let bytes = reqwest::get(&url).await .map_err(|e| format!("Thumbnail request failed: {}", e))? .bytes().await .map_err(|e| format!("Thumbnail read failed: {}", e))?; Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))) } #[tauri::command(rename_all = "camelCase")] async fn get_video_stream(uuid: String, start_time: f64, end_time: f64) -> Result { let url = format!("{}/api/v1/file/{}/video?api_key={}&start_time={}&end_time={}", CORE_API, uuid, API_KEY, start_time, end_time); let bytes = reqwest::get(&url).await .map_err(|e| format!("Video request failed: {}", e))? .bytes().await .map_err(|e| format!("Video read failed: {}", e))?; let tmp_dir = std::env::temp_dir(); let file_path = tmp_dir.join(format!("momentry_{}.mp4", uuid)); std::fs::write(&file_path, &bytes) .map_err(|e| format!("Write temp file failed: {}", e))?; Ok(format!("file://{}", file_path.display())) } const PROFILE_DIRS: &[&str] = &[ "/Users/accusys/momentry/output/identities", "/Users/accusys/momentry/output_dev/identities", ]; #[tauri::command(rename_all = "camelCase")] async fn get_identity_profile(uuid: String) -> Result { let no_dash = uuid.replace('-', ""); for dir in PROFILE_DIRS { let path = format!("{}/{}/profile.jpg", dir, no_dash); if std::path::Path::new(&path).exists() { let bytes = std::fs::read(&path).map_err(|e| format!("Read failed: {}", e))?; return Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); } let path = format!("{}/{}/profile.jpg", dir, uuid); if std::path::Path::new(&path).exists() { let bytes = std::fs::read(&path).map_err(|e| format!("Read failed: {}", e))?; return Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); } } 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 = reqwest::Client::new(); 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 = reqwest::Client::new(); let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY); let resp = client.patch(&url) .json(&serde_json::json!({"status": status})) .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 delete_identity(uuid: String) -> Result<(), String> { let client = reqwest::Client::new(); 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(()) } #[tauri::command(rename_all = "camelCase")] async fn search_identities(query: String, limit: usize) -> Result, String> { let client = reqwest::Client::new(); let url = format!("{}/api/v1/identities/search?api_key={}&q={}", CORE_API, API_KEY, query); 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 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()), 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 = reqwest::Client::new(); 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), }) }) .collect(); Ok(candidates) } #[tauri::command(rename_all = "camelCase")] async fn merge_identities(uuid: String, into_uuid: String) -> Result<(), String> { let client = reqwest::Client::new(); 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: String, file_uuid: String) -> Result<(), String> { let client = reqwest::Client::new(); let url = format!("{}/api/v1/identity/{}/bind?api_key={}", CORE_API, uuid, API_KEY); let resp = client.post(&url) .json(&serde_json::json!({"face_id": face_id, "file_uuid": file_uuid})) .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: String, file_uuid: String) -> Result<(), String> { let client = reqwest::Client::new(); let url = format!("{}/api/v1/identity/{}/unbind?api_key={}", CORE_API, uuid, API_KEY); let resp = client.post(&url) .json(&serde_json::json!({"face_id": face_id, "file_uuid": file_uuid})) .send().await .map_err(|e| format!("Request failed: {}", e))?; if !resp.status().is_success() { return Err(format!("Unbind failed: {}", resp.status())); } Ok(()) } fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ search_llm_smart, get_files, get_people, get_faces, get_traces, get_thumbnail, get_video_stream, get_identity_profile, update_identity_name, update_identity_status, delete_identity, search_identities, get_face_candidates, merge_identities, bind_face, unbind_face ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }