449 lines
16 KiB
Rust
449 lines
16 KiB
Rust
#![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<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FileInfo {
|
|
file_uuid: String,
|
|
file_name: String,
|
|
file_size: i64,
|
|
modified_time: String,
|
|
#[serde(rename = "isRegistered")]
|
|
is_registered: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct PersonInfo {
|
|
identity_uuid: String,
|
|
name: String,
|
|
starred: bool,
|
|
status: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FaceInfo {
|
|
id: i64,
|
|
file_uuid: String,
|
|
frame_number: i64,
|
|
timestamp_secs: f64,
|
|
face_id: Option<String>,
|
|
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<String>,
|
|
file_uuid: String,
|
|
frame_number: i64,
|
|
confidence: f64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct SearchIdentityResult {
|
|
identity_id: i64,
|
|
name: String,
|
|
source: String,
|
|
tmdb_id: Option<i64>,
|
|
file_uuid: Option<String>,
|
|
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 = 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<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()?,
|
|
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<Vec<FileInfo>, 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<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();
|
|
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<Vec<PersonInfo>, String> {
|
|
eprintln!("[get_people] called");
|
|
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),
|
|
status: "confirmed".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
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<Vec<FaceInfo>, 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<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),
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(faces)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
async fn get_traces(uuid: String, per_page: usize) -> Result<Vec<TraceInfo>, 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<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(),
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<Vec<SearchIdentityResult>, 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<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()),
|
|
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 = 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<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),
|
|
})
|
|
})
|
|
.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");
|
|
}
|