feat: add migrations, test scripts, and utility tools

- Add database migrations (006-028) for face recognition, identity, file_uuid
- Add test scripts for ASR, face, search, processing
- Add portal frontend (Tauri)
- Add config, benchmark, and monitoring utilities
- Add model checkpoints and pretrained model references
This commit is contained in:
Warren
2026-04-30 15:11:53 +08:00
parent 4d75b2e251
commit b54c2def30
192 changed files with 46721 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
use crate::config::{get_config, PortalConfig};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub uptime_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DetailedHealthResponse {
pub status: String,
pub version: String,
pub uptime_ms: u64,
pub services: ServiceHealth,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceHealth {
pub postgres: ServiceStatus,
pub redis: ServiceStatus,
pub qdrant: ServiceStatus,
pub mongodb: ServiceStatus,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceStatus {
pub status: String,
pub latency_ms: Option<u64>,
pub error: Option<String>,
}
#[tauri::command]
pub async fn get_health() -> Result<HealthResponse, String> {
let config = get_config();
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/health", config.api_base_url))
.timeout(std::time::Duration::from_secs(5))
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: HealthResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[tauri::command]
pub async fn get_health_detailed() -> Result<DetailedHealthResponse, String> {
let config = get_config();
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/health/detailed", config.api_base_url))
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: DetailedHealthResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SftpgoStatus {
pub username: String,
pub home_dir: String,
pub files_count: i64,
pub registered_videos: Vec<RegisteredVideo>,
pub last_login: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisteredVideo {
pub uuid: String,
pub file_name: String,
pub status: String,
}
#[tauri::command]
pub async fn get_sftpgo_status() -> Result<SftpgoStatus, String> {
let config = get_config();
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/v1/stats/sftpgo", config.api_base_url))
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: SftpgoStatus = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InferenceEngineStatus {
pub engine: String,
pub model: String,
pub status: String,
pub latency_ms: Option<u64>,
pub error: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InferenceHealthResponse {
pub ollama: InferenceEngineStatus,
pub llama_server: InferenceEngineStatus,
}
#[tauri::command]
pub async fn get_inference_health() -> Result<InferenceHealthResponse, String> {
let config = get_config();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| format!("Client build failed: {}", e))?;
let response = client
.get(&format!("{}/api/v1/stats/inference", config.api_base_url))
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: InferenceHealthResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[tauri::command]
pub fn get_config_info() -> Result<PortalConfig, String> {
Ok(get_config())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IngestStats {
pub total_videos: i64,
pub total_chunks: i64,
pub sentence_chunks: i64,
pub cut_chunks: i64,
pub time_chunks: i64,
pub searchable_chunks: i64,
pub chunks_with_visual: i64,
pub chunks_with_summary: i64,
pub pending_videos: i64,
}
#[tauri::command]
pub async fn get_ingest_stats() -> Result<IngestStats, String> {
let config = get_config();
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/v1/stats/ingest", config.api_base_url))
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: IngestStats = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}

View File

@@ -0,0 +1,148 @@
use crate::config::get_config;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentitySearchRequest {
pub query: Option<String>,
pub file_uuid: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentityResponse {
pub success: bool,
pub total: usize,
pub identities: Vec<IdentityItem>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentityItem {
pub id: i32,
pub person_id: String,
pub face_identity_id: Option<i32>,
pub file_uuid: String,
pub profile: IdentityProfile,
pub stats: IdentityStats,
pub is_confirmed: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentityProfile {
pub name: Option<String>,
pub original_name: Option<String>,
pub character_name: Option<String>,
pub speaker_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdentityStats {
pub appearance_count: i32,
pub total_duration: f64,
pub first_appearance: Option<f64>,
pub last_appearance: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterResponse {
pub success: bool,
pub message: String,
pub person_id: String,
pub name: String,
pub face_identity_id: i32,
}
#[tauri::command]
pub async fn list_identities(
query: Option<String>,
file_uuid: Option<String>,
) -> Result<IdentityResponse, String> {
let config = get_config();
let client = reqwest::Client::new();
let request_body = IdentitySearchRequest {
query,
file_uuid,
limit: Some(50),
};
let response = client
.post(&format!("{}/api/v1/identities/search", config.api_base_url))
.header("Content-Type", "application/json")
.header("x-api-key", &config.api_key)
.json(&request_body)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: IdentityResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[tauri::command]
pub async fn register_identity(
person_id: String,
file_uuid: String,
) -> Result<RegisterResponse, String> {
let config = get_config();
let client = reqwest::Client::new();
let url = format!(
"{}/api/v1/person/{}/register?file_uuid={}",
config.api_base_url, person_id, file_uuid
);
let response = client
.post(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: RegisterResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
#[tauri::command]
pub async fn get_identity_videos(identity_id: i32) -> Result<serde_json::Value, String> {
let config = get_config();
let client = reqwest::Client::new();
let url = format!(
"{}/api/v1/identities/{}/videos",
config.api_base_url, identity_id
);
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let result: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}

View File

@@ -0,0 +1,6 @@
pub mod health;
pub mod identity;
pub mod person;
pub mod search;
pub mod translation;
pub mod video;

View File

@@ -0,0 +1,84 @@
use crate::config::get_config;
use base64::{engine::general_purpose, Engine as _};
#[tauri::command]
pub async fn get_person_thumbnail(
person_id: String,
file_uuid: String,
index: Option<usize>,
save_path: String,
) -> Result<String, String> {
let config = get_config();
let client = reqwest::Client::new();
let mut url = format!(
"{}/api/v1/person/{}/thumbnail?file_uuid={}",
config.api_base_url, person_id, file_uuid
);
if let Some(idx) = index {
url = format!("{}&index={}", url, idx);
}
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
// Save the image to the specified path
let bytes = response
.bytes()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
tokio::fs::write(&save_path, &bytes)
.await
.map_err(|e| format!("Failed to save file: {}", e))?;
Ok(save_path)
}
/// Get person thumbnail as base64 data URI
#[tauri::command]
pub async fn get_person_thumbnail_b64(
person_id: String,
file_uuid: String,
index: Option<usize>,
) -> Result<String, String> {
let config = get_config();
let client = reqwest::Client::new();
let mut url = format!(
"{}/api/v1/person/{}/thumbnail?file_uuid={}",
config.api_base_url, person_id, file_uuid
);
if let Some(idx) = index {
url = format!("{}&index={}", url, idx);
}
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
let bytes = response
.bytes()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
let encoded = general_purpose::STANDARD.encode(&bytes);
Ok(format!("data:image/jpeg;base64,{}", encoded))
}

View File

@@ -0,0 +1,175 @@
use crate::config::get_config;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchRequest {
pub query: String,
pub limit: Option<usize>,
pub mode: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
pub query: String,
pub count: usize,
pub hits: Vec<SearchHit>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchHit {
pub id: String,
pub vid: String,
pub start_frame: i64,
pub end_frame: i64,
pub fps: f64,
pub start: f64,
pub end: f64,
pub text: String,
pub score: f64,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub has_visual_stats: Option<bool>,
#[serde(default)]
pub parent_id: Option<String>,
}
#[tauri::command]
pub async fn search_videos(
query: String,
limit: Option<usize>,
mode: Option<String>,
) -> Result<SearchResult, String> {
let config = get_config();
let client = reqwest::Client::new();
let search_mode = mode.unwrap_or_else(|| "vector".to_string());
let request_body = SearchRequest {
query: query.clone(),
limit: limit.or(Some(10)),
mode: Some(search_mode.clone()),
};
let url = format!("{}/api/v1/search", config.api_base_url);
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("x-api-key", &config.api_key)
.json(&request_body)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
// Parse as generic Value to handle mapping manually
let json: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
// Map Backend Response to Frontend SearchResult
// Backend: { "query": "...", "results": [ ... ], "total": N, ... }
// Frontend: { "query": "...", "hits": [ ... ], "count": N }
let backend_results = json.get("results").and_then(|r| r.as_array()).cloned().unwrap_or_default();
let total = json.get("total").and_then(|t| t.as_u64()).unwrap_or(0) as usize;
let hits: Vec<SearchHit> = backend_results.into_iter().filter_map(|item| {
Some(SearchHit {
id: item.get("chunk_id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vid: item.get("uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(),
start_frame: item.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0),
end_frame: item.get("end_frame").and_then(|v| v.as_i64()).unwrap_or(0),
fps: item.get("fps").and_then(|v| v.as_f64()).unwrap_or(30.0),
start: item.get("start_time").and_then(|v| v.as_f64()).unwrap_or(0.0),
end: item.get("end_time").and_then(|v| v.as_f64()).unwrap_or(0.0),
text: item.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string(),
score: item.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0),
title: item.get("file_name").and_then(|v| v.as_str()).map(|s| s.to_string()),
file_path: item.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string()),
has_visual_stats: item.get("visual_stats").map(|_| true),
parent_id: item.get("parent_chunk_id").and_then(|v| v.as_str()).map(|s| s.to_string()),
})
}).collect();
Ok(SearchResult {
query: json.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string(),
count: total,
hits,
})
}
#[tauri::command]
pub async fn search_chunks(query: String, uuid: Option<String>) -> Result<SearchResult, String> {
let config = get_config();
let client = reqwest::Client::new();
// Backend expects uuid in the body, not query params
let url = format!("{}/api/v1/search", config.api_base_url);
let mut request_body = serde_json::json!({
"query": query,
"limit": 10
});
if let Some(vid) = uuid {
request_body["uuid"] = serde_json::json!(vid);
}
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("x-api-key", &config.api_key)
.json(&request_body)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
// Parse raw JSON to handle structure mapping
let json: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
// Backend returns "total", frontend expects "count"
let count = json.get("total").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
// Backend returns "results", frontend expects "hits"
let results = json.get("results").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let hits: Vec<SearchHit> = results.into_iter().filter_map(|item| {
Some(SearchHit {
id: item.get("chunk_id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
vid: item.get("uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(),
start_frame: item.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0),
end_frame: item.get("end_frame").and_then(|v| v.as_i64()).unwrap_or(0),
fps: item.get("fps").and_then(|v| v.as_f64()).unwrap_or(30.0),
start: item.get("start_time").and_then(|v| v.as_f64()).unwrap_or(0.0),
end: item.get("end_time").and_then(|v| v.as_f64()).unwrap_or(0.0),
text: item.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string(),
score: item.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0),
title: item.get("file_name").and_then(|v| v.as_str()).map(|s| s.to_string()),
file_path: item.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string()),
has_visual_stats: item.get("visual_stats").map(|_| true),
parent_id: item.get("parent_chunk_id").and_then(|v| v.as_str()).map(|s| s.to_string()),
})
}).collect();
Ok(SearchResult {
query: json.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string(),
count,
hits,
})
}

View File

@@ -0,0 +1,81 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct LlamaCppResponse {
choices: Vec<LlamaCppChoice>,
}
#[derive(Debug, Deserialize, Serialize)]
struct LlamaCppChoice {
message: LlamaCppMessage,
}
#[derive(Debug, Deserialize, Serialize)]
struct LlamaCppMessage {
content: String,
}
/// Translates text using local llama.cpp server running Gemma 4.
#[tauri::command]
pub async fn translate_text(
text: String,
#[allow(non_snake_case)] target_lang: String,
) -> Result<String, String> {
if text.trim().is_empty() {
return Ok(String::new());
}
println!("[Translate] Request: {} -> {}", target_lang, text);
let client = reqwest::Client::new();
let prompt = format!(
"Translate the following text into {}. Only output the translated text without any additional context or notes.\n\nText: {}",
target_lang, text
);
// llama.cpp server endpoint (compatible with OpenAI API format)
let payload = serde_json::json!({
"model": "gemma4",
"messages": [
{
"role": "user",
"content": prompt
}
],
"stream": false,
"temperature": 0.1
});
println!("[Translate] Sending to llama.cpp server...");
let response = client
.post("http://127.0.0.1:8081/v1/chat/completions")
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
.map_err(|e| {
format!(
"llama.cpp server request failed: {}. Ensure the server is running at port 8081.",
e
)
})?;
if !response.status().is_success() {
return Err(format!("llama.cpp server error: {}", response.status()));
}
let json: LlamaCppResponse = response
.json()
.await
.map_err(|e| format!("Parse error: {}", e))?;
println!("[Translate] Response received");
if let Some(choice) = json.choices.first() {
Ok(choice.message.content.trim().to_string())
} else {
Err("No translation result returned".to_string())
}
}

View File

@@ -0,0 +1,119 @@
use crate::config::get_config;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct VideosResponse {
pub videos: Vec<serde_json::Value>,
#[serde(rename = "count", default)]
pub total: i64,
pub page: usize,
pub page_size: usize,
}
#[tauri::command]
pub async fn get_videos(
query: Option<String>,
status: Option<String>,
page: Option<usize>,
page_size: Option<usize>,
) -> Result<VideosResponse, String> {
let config = get_config();
let client = reqwest::Client::new();
let mut url = format!("{}/api/v1/videos", config.api_base_url);
let mut params = Vec::new();
if let Some(q) = query {
params.push(format!("q={}", q));
}
if let Some(s) = status {
params.push(format!("status={}", s));
}
if let Some(p) = page {
params.push(format!("page={}", p));
}
if let Some(ps) = page_size {
params.push(format!("page_size={}", ps));
}
if !params.is_empty() {
url.push('?');
url.push_str(&params.join("&"));
}
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request to API failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API returned error: {}", response.status()));
}
let result: VideosResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse API response: {}", e))?;
Ok(result)
}
#[tauri::command]
pub async fn list_videos(
query: Option<String>,
page: Option<usize>,
page_size: Option<usize>,
) -> Result<VideosResponse, String> {
get_videos(query, None, page, page_size).await
}
#[tauri::command]
pub async fn get_video_faces(file_uuid: String) -> Result<serde_json::Value, String> {
let config = get_config();
let client = reqwest::Client::new();
let url = format!("{}/api/v1/videos/{}/faces", config.api_base_url, file_uuid);
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))
}
#[tauri::command]
pub async fn get_chunk_detail(uuid: String, chunk_id: String) -> Result<serde_json::Value, String> {
let config = get_config();
let client = reqwest::Client::new();
let url = format!(
"{}/api/v1/videos/{}/details?chunk_id={}",
config.api_base_url, uuid, chunk_id
);
let response = client
.get(&url)
.header("x-api-key", &config.api_key)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()));
}
response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))
}

View File

@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortalConfig {
pub api_base_url: String,
pub api_key: String,
pub timeout_secs: u64,
}
impl Default for PortalConfig {
fn default() -> Self {
Self {
api_base_url: "http://127.0.0.1:3003".to_string(),
api_key: "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69".to_string(),
timeout_secs: 30,
}
}
}
static CONFIG: Mutex<Option<PortalConfig>> = Mutex::new(None);
pub fn init_config() {
let mut config = CONFIG.lock().unwrap();
if config.is_none() {
let api_url = std::env::var("MOMENTRY_API_URL")
.unwrap_or_else(|_| "http://127.0.0.1:3003".to_string());
let api_key = std::env::var("MOMENTRY_API_KEY").unwrap_or_else(|_| {
"muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69".to_string()
});
*config = Some(PortalConfig {
api_base_url: api_url,
api_key,
timeout_secs: 30,
});
}
}
pub fn get_config() -> PortalConfig {
let config = CONFIG.lock().unwrap();
config.clone().unwrap_or_default()
}

View File

@@ -0,0 +1,7 @@
pub mod config;
pub mod api {
pub mod search;
pub mod identity;
pub mod video;
pub mod person;
}

View File

@@ -0,0 +1,84 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod api;
mod config;
use std::sync::atomic::{AtomicU32, Ordering};
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut};
#[tauri::command]
fn open_devtools(app: tauri::AppHandle) {
#[cfg(debug_assertions)]
if let Some(window) = app.get_webview_window("main") {
let _ = window.open_devtools();
}
}
fn main() {
// Define zoom level in steps of 10% (100 = 1.0x)
static ZOOM_LEVEL: AtomicU32 = AtomicU32::new(100);
let zoom_in = Shortcut::new(Some(Modifiers::SUPER), Code::Equal); // Cmd + =
let zoom_out = Shortcut::new(Some(Modifiers::SUPER), Code::Minus); // Cmd + -
let zoom_reset = Shortcut::new(Some(Modifiers::SUPER), Code::Digit0); // Cmd + 0
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_fs::init())
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |app, _shortcut, _event| {
let window = app.get_webview_window("main").unwrap();
let current = ZOOM_LEVEL.load(Ordering::SeqCst);
if _shortcut.id() == zoom_in.id() {
let new_zoom = (current + 10).min(200); // Max 200%
ZOOM_LEVEL.store(new_zoom, Ordering::SeqCst);
let _ = window.set_zoom(new_zoom as f64 / 100.0);
} else if _shortcut.id() == zoom_out.id() {
let new_zoom = (current - 10).max(50); // Min 50%
ZOOM_LEVEL.store(new_zoom, Ordering::SeqCst);
let _ = window.set_zoom(new_zoom as f64 / 100.0);
} else if _shortcut.id() == zoom_reset.id() {
ZOOM_LEVEL.store(100, Ordering::SeqCst);
let _ = window.set_zoom(1.0);
}
})
.build(),
)
.setup(move |app| {
config::init_config();
app.global_shortcut().register(zoom_in)?;
app.global_shortcut().register(zoom_out)?;
app.global_shortcut().register(zoom_reset)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
api::health::get_health,
api::health::get_health_detailed,
api::health::get_config_info,
api::health::get_ingest_stats,
api::health::get_sftpgo_status,
api::health::get_inference_health,
api::search::search_videos,
api::search::search_chunks,
api::identity::list_identities,
api::identity::register_identity,
api::identity::get_identity_videos,
api::video::list_videos,
api::video::get_videos,
api::video::get_video_faces,
api::video::get_chunk_detail,
api::person::get_person_thumbnail,
api::person::get_person_thumbnail_b64,
api::translation::translate_text,
open_devtools,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}