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:
203
portal/src-tauri/src/api/health.rs
Normal file
203
portal/src-tauri/src/api/health.rs
Normal 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)
|
||||
}
|
||||
148
portal/src-tauri/src/api/identity.rs
Normal file
148
portal/src-tauri/src/api/identity.rs
Normal 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)
|
||||
}
|
||||
6
portal/src-tauri/src/api/mod.rs
Normal file
6
portal/src-tauri/src/api/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod health;
|
||||
pub mod identity;
|
||||
pub mod person;
|
||||
pub mod search;
|
||||
pub mod translation;
|
||||
pub mod video;
|
||||
84
portal/src-tauri/src/api/person.rs
Normal file
84
portal/src-tauri/src/api/person.rs
Normal 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))
|
||||
}
|
||||
175
portal/src-tauri/src/api/search.rs
Normal file
175
portal/src-tauri/src/api/search.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
81
portal/src-tauri/src/api/translation.rs
Normal file
81
portal/src-tauri/src/api/translation.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
119
portal/src-tauri/src/api/video.rs
Normal file
119
portal/src-tauri/src/api/video.rs
Normal 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(¶ms.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))
|
||||
}
|
||||
Reference in New Issue
Block a user