feat: Library 批量注册 + PeopleView 修复 + 国际化

Library:
- 添加 checkbox 和鼠标框选功能
- Unregistered 卡片添加快速注册按钮
- 状态标签移至左下方,人脸图标移至左侧中间
- Deep Scan 改为 unregister -> register -> process
- 完整国际化(日语、繁体中文、英语)

PeopleView:
- 修复卡片不显示问题(使用 get_file_traces API)
- 移除对 identity_id 字段的依赖

其他:
- 添加日语语言包
- 添加 useDragSelect composable
- 新增文档:Face Bbox Null Bug Report
This commit is contained in:
2026-10-01 00:42:13 +08:00
parent 7b60ca3d2d
commit f3d75ebce8
35 changed files with 8054 additions and 961 deletions

View File

@@ -1,22 +1,25 @@
use axum::{
body::Body,
extract::State,
extract::{State, Query},
http::{HeaderValue, StatusCode},
response::{IntoResponse, Response},
Router,
};
use tower_http::cors::{Any, CorsLayer};
use crate::db;
#[path = "validate.rs"]
mod validate;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use image::GenericImageView;
use image::DynamicImage;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::num::NonZeroUsize;
use lru::LruCache;
use tokio::sync::Semaphore;
const CORE_API: &str = "http://localhost:3002";
const CORE_API: &str = "http://127.0.0.1:3002";
const MARKBASE_API: &str = "http://127.0.0.1:11438";
const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69";
const DIST_DIR: &str = "../dist";
const PROFILE_DIRS: [&str; 4] = [
@@ -28,7 +31,6 @@ const PROFILE_DIRS: [&str; 4] = [
static FACE_THUMB_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
static PROFILE_PROXY_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
static RAW_DIM_PROXY_CACHE: Mutex<Option<HashMap<String, (u32, u32)>>> = Mutex::new(None);
static THUMB_SEM_MAP_PROXY: Mutex<Option<HashMap<String, Arc<Semaphore>>>> = Mutex::new(None);
fn acquire_thumb_semaphore(uuid: &str) -> Arc<Semaphore> {
@@ -84,11 +86,16 @@ pub async fn start_proxy_server() {
.route("/api/v1/face-thumbnail", axum::routing::get(get_face_thumbnail_handler))
.route("/api/v1/media/frame", axum::routing::get(get_frame_handler))
.route("/api/v1/identity-matches", axum::routing::get(get_identity_matches_handler))
.route("/api/v1/trace-profile-file", axum::routing::get(get_trace_profile_file_handler))
.route("/api/v1/cluster-results", axum::routing::get(get_cluster_results_handler))
.route("/api/v1/processor-json", axum::routing::get(get_processor_json_handler))
.route("/api/v1/file/thumbnail", axum::routing::get(get_file_thumbnail_by_path_handler))
.route("/api/v1/file/:file_uuid/pose", axum::routing::get(get_pose_handler))
.route("/api/v1/file/:file_uuid/appearance", axum::routing::get(get_appearance_handler))
.route("/api/v1/admin/config", axum::routing::get(get_admin_config_handler))
.route("/api/v1/admin/config", axum::routing::post(save_admin_config_handler))
.route("/api/v1/system-stats", axum::routing::get(get_system_stats_handler))
.route("/api/v1/system-stats/validate", axum::routing::get(validate_system_stats_handler))
.fallback(fallback_handler)
.layer(cors)
.with_state(state);
@@ -206,7 +213,7 @@ async fn fallback_handler(State(state): State<ProxyState>, req: axum::extract::R
if let Some(rest) = path.strip_prefix("/api/v1/identity/") {
if rest.ends_with("/profile-image") {
let uuid = rest.trim_end_matches("/profile-image").to_string();
let response = proxy_api(state, req).await;
let response = proxy_to(state, req, CORE_API, true).await;
// Invalidate profile cache on successful upload
let status = response.status();
if status == StatusCode::OK || status == StatusCode::CREATED {
@@ -220,12 +227,14 @@ async fn fallback_handler(State(state): State<ProxyState>, req: axum::extract::R
}
// Handle search-history CRUD locally
if path.starts_with("/api/v1/search-history") || path.starts_with("/api/v1/bookmarks") {
if path.starts_with("/api/v1/search-history") || path.starts_with("/api/v1/bookmarks") || path.starts_with("/api/v1/video-marks") || path.starts_with("/api/v1/video-mark/") {
return handle_local_api(req).await;
}
if path.starts_with("/api/") {
return proxy_api(state, req).await;
if path.starts_with("/api/v2/") {
return proxy_to(state, req, MARKBASE_API, false).await;
} else if path.starts_with("/api/") {
return proxy_to(state, req, CORE_API, true).await;
} else if state.serve_static {
return serve_spa(&path).await;
} else {
@@ -235,6 +244,7 @@ async fn fallback_handler(State(state): State<ProxyState>, req: axum::extract::R
async fn handle_local_api(req: axum::extract::Request) -> Response {
let path = req.uri().path().to_string();
let query_str = req.uri().query().unwrap_or("").to_string();
let method = req.method().clone();
let body_bytes = axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await.unwrap_or_default();
@@ -271,26 +281,46 @@ async fn handle_local_api(req: axum::extract::Request) -> Response {
return save_bookmark_handler_inner(body_bytes);
}
// Video marks (local SQLite)
if method == axum::http::Method::GET && path == "/api/v1/video-marks" {
return get_video_marks_handler_inner(&query_str);
}
if method == axum::http::Method::POST && path == "/api/v1/video-marks" {
return save_video_mark_handler_inner(body_bytes);
}
if method == axum::http::Method::DELETE {
if let Some(id_str) = path.strip_prefix("/api/v1/video-mark/") {
if let Ok(id) = id_str.parse::<i64>() {
return delete_video_mark_handler_inner(id);
}
}
}
(StatusCode::NOT_FOUND, "Not found").into_response()
}
async fn proxy_api(state: ProxyState, req: axum::extract::Request) -> Response {
async fn proxy_to(state: ProxyState, req: axum::extract::Request, base: &str, inject_api_key: bool) -> Response {
let path = req.uri().path().to_string();
let method = req.method().clone();
let headers = req.headers().clone();
let start = std::time::Instant::now();
let query_str = req.uri().query().unwrap_or("").to_string();
let mut url = format!("{}{}?api_key={}", CORE_API, path, API_KEY);
let mut url = if inject_api_key {
format!("{}{}?api_key={}", base, path, API_KEY)
} else {
format!("{}{}", base, path)
};
for pair in query_str.split('&') {
if pair.is_empty() || pair.starts_with("api_key=") {
if pair.is_empty() || (inject_api_key && pair.starts_with("api_key=")) {
continue;
}
url.push_str(&format!("&{}", pair));
}
let target = if base == CORE_API { "core api" } else { "markbase" };
let range_hdr = headers.get("range").and_then(|v| v.to_str().ok()).unwrap_or("-");
eprintln!("[proxy] --> {} {} (core api) range={}", method, path, range_hdr);
eprintln!("[proxy] --> {} {} ({}) range={}", method, path, target, range_hdr);
let reqwest_method = match method.as_str() {
"POST" => reqwest::Method::POST,
@@ -537,6 +567,45 @@ fn delete_bookmark_handler_inner(id: i64) -> Response {
}
}
fn get_video_marks_handler_inner(query_str: &str) -> Response {
let file_uuid = query_str
.split('&')
.find_map(|pair| {
let mut it = pair.splitn(2, '=');
let k = it.next().unwrap_or("");
let v = it.next().unwrap_or("");
if k == "file_uuid" { Some(v.to_string()) } else { None }
})
.unwrap_or_default();
match db::get_video_marks(file_uuid) {
Ok(items) => axum::Json(items).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
fn save_video_mark_handler_inner(body: axum::body::Bytes) -> Response {
let req: serde_json::Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)).into_response(),
};
let file_uuid = req.get("file_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string();
let tag = req.get("tag").and_then(|v| v.as_str()).unwrap_or("default").to_string();
let start_frame = req.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0);
let end_frame = req.get("end_frame").and_then(|v| v.as_i64());
let note = req.get("note").and_then(|v| v.as_str()).map(|s| s.to_string());
match db::save_video_mark(file_uuid, tag, start_frame, end_frame, note) {
Ok(item) => axum::Json(item).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
fn delete_video_mark_handler_inner(id: i64) -> Response {
match db::delete_video_mark(id) {
Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn get_identity_profile_handler_inner(uuid: String) -> Response {
let start = std::time::Instant::now();
let no_dash = uuid.replace('-', "");
@@ -579,33 +648,6 @@ async fn get_identity_profile_handler_inner(uuid: String) -> Response {
}
}
/// Read EXIF orientation and physically rotate pixels, stripping orientation tag.
/// Fetch raw video dimensions from Core API with in-memory cache.
async fn fetch_raw_dimensions(uuid: &str) -> Option<(u32, u32)> {
{
let cache = RAW_DIM_PROXY_CACHE.lock().unwrap();
if let Some(cache) = cache.as_ref() {
if let Some(dims) = cache.get(uuid) {
return Some(*dims);
}
}
}
let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, uuid, API_KEY);
let result = reqwest::get(&url).await.ok()?
.json::<serde_json::Value>().await.ok()?;
let w = result["width"].as_u64().unwrap_or(0) as u32;
let h = result["height"].as_u64().unwrap_or(0) as u32;
if w == 0 || h == 0 {
return None;
}
{
let mut cache = RAW_DIM_PROXY_CACHE.lock().unwrap();
let cache = cache.get_or_insert_with(HashMap::new);
cache.insert(uuid.to_string(), (w, h));
}
Some((w, h))
}
async fn get_face_thumbnail_handler(State(state): State<ProxyState>, axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
@@ -729,6 +771,46 @@ async fn get_identity_matches_handler(axum::extract::Query(params): axum::extrac
}
}
async fn get_trace_profile_file_handler(axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
let file_uuid = match params.get("file_uuid") {
Some(v) => v.clone(),
None => return (StatusCode::BAD_REQUEST, "Missing file_uuid").into_response(),
};
let trace_id = match params.get("trace_id") {
Some(v) => v.clone(),
None => return (StatusCode::BAD_REQUEST, "Missing trace_id").into_response(),
};
let output_bases = [
"/Users/accusys/momentry/output",
"/Users/accusys/momentry/output_dev",
"/Volumes/external/momentry/output",
"/Volumes/external/momentry/output_dev",
];
let mut profile_path = None;
for base in &output_bases {
let path = format!("{}/{}/trace_{}/trace_profile.json", base, file_uuid, trace_id);
if std::path::Path::new(&path).exists() {
profile_path = Some(path);
break;
}
}
match profile_path {
Some(path) => match std::fs::read_to_string(&path) {
Ok(content) => {
let parsed: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response();
}
};
axum::Json(parsed).into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(),
},
None => (StatusCode::NOT_FOUND, "Trace profile file not found").into_response(),
}
}
async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
let file_hash = match params.get("file_hash") {
Some(v) => v.clone(),
@@ -736,29 +818,46 @@ async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract
};
let client = reqwest::Client::new();
let url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash);
let resp = match client
.get(&url)
.header("X-API-Key", API_KEY)
.send()
.await
{
// Try new cluster-results endpoint first, fallback to face-groups
let url = format!("{}/api/v1/cluster-results?file_hash={}", CORE_API, file_hash);
let resp = client.get(&url).header("X-API-Key", API_KEY).send().await;
match resp {
Ok(r) => {
let status = r.status();
let b = r.text().await.unwrap_or_default();
// Check if response has data
if status.is_success() {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&b) {
let fg = v.get("face_groups").and_then(|x| x.as_array()).map(|x| x.len()).unwrap_or(0);
if fg > 0 {
return ([(axum::http::header::CONTENT_TYPE, "application/json")], b).into_response();
}
}
}
}
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Request failed: {}", e)).into_response(),
};
// Fallback to face-groups
let fg_url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash);
let fg_resp = match client.get(&fg_url).header("X-API-Key", API_KEY).send().await {
Ok(r) => r,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Face groups request failed: {}", e)).into_response(),
};
let status = resp.status();
let body = match resp.text().await {
let fg_status = fg_resp.status();
let fg_body = match fg_resp.text().await {
Ok(b) => b,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(),
};
let axum_status = axum::http::StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let axum_status = axum::http::StatusCode::from_u16(fg_status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
if !axum_status.is_success() {
return (axum_status, body).into_response();
return (axum_status, fg_body).into_response();
}
let result: serde_json::Value = match serde_json::from_str(&body) {
let result: serde_json::Value = match serde_json::from_str(&fg_body) {
Ok(v) => v,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON parse failed: {}", e)).into_response(),
};
@@ -907,12 +1006,11 @@ async fn get_frame_handler(State(state): State<ProxyState>, axum::extract::Query
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(),
}
} else {
let fps = file_info["fps"].as_f64().unwrap_or(24.0);
let time_sec = frame as f64 / fps;
// Use select filter for exact frame extraction instead of -ss seeking
let output = tokio::process::Command::new("ffmpeg")
.args([
"-ss", &time_sec.to_string(),
"-i", &file_path,
"-vf", &format!("select=eq(n\\,{frame})"),
"-vframes", "1",
"-q:v", "2",
"-f", "mjpeg",
@@ -1015,4 +1113,288 @@ async fn get_appearance_handler(
};
(status, body).into_response()
}
}
const ADMIN_CONFIG_PATH: &str = "/Users/accusys/momentry/config/admin_config.json";
async fn get_admin_config_handler() -> Response {
match std::fs::read_to_string(ADMIN_CONFIG_PATH) {
Ok(content) => {
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(v) => axum::Json(v).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(),
}
}
Err(e) => (StatusCode::NOT_FOUND, format!("Config file not found: {}", e)).into_response(),
}
}
async fn save_admin_config_handler(axum::extract::Json(payload): axum::extract::Json<serde_json::Value>) -> Response {
match serde_json::to_string_pretty(&payload) {
Ok(formatted) => {
match std::fs::write(ADMIN_CONFIG_PATH, formatted) {
Ok(()) => axum::Json(serde_json::json!({"success": true, "message": "Config saved"})).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Write failed: {}", e)).into_response(),
}
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Format failed: {}", e)).into_response(),
}
}
#[cfg(target_arch = "aarch64")]
mod macmon_sampler {
use macmon::Sampler;
pub struct MacmonData {
pub cpu_power: f32,
pub gpu_power: f32,
pub ane_power: f32,
pub sys_power: f32,
pub ram_power: f32,
pub cpu_temp: f32,
pub gpu_temp: f32,
pub ecpu_freq_mhz: u32,
pub pcpu_freq_mhz: u32,
pub gpu_freq_mhz: u32,
pub cpu_scaled_ratio: f32,
pub gpu_scaled_ratio: f32,
pub chip_name: String,
pub ecpu_cores: u8,
pub pcpu_cores: u8,
pub gpu_cores: u8,
pub ram_total: u64,
pub ram_usage: u64,
pub swap_total: u64,
pub swap_used: u64,
}
fn get_swap_info() -> (u64, u64) {
use std::process::Command;
let output = match Command::new("sysctl")
.args(["-n", "vm.swapusage"])
.output()
{
Ok(o) => o,
Err(_) => return (0, 0),
};
let s = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = s.split_whitespace().collect();
let total_mb = parts.get(2)
.and_then(|v| v.trim_end_matches('M').parse::<f64>().ok())
.unwrap_or(0.0) as u64;
let used_mb = parts.get(5)
.and_then(|v| v.trim_end_matches('M').parse::<f64>().ok())
.unwrap_or(0.0) as u64;
(total_mb, used_mb)
}
pub fn get_metrics() -> Option<MacmonData> {
let mut sampler = Sampler::new().ok()?;
let metrics = sampler.get_metrics(100).ok()?;
let soc = sampler.get_soc_info();
let (swap_total, swap_used) = get_swap_info();
Some(MacmonData {
cpu_power: metrics.cpu_power,
gpu_power: metrics.gpu_power,
ane_power: metrics.ane_power,
sys_power: metrics.sys_power,
ram_power: metrics.ram_power,
cpu_temp: metrics.temp.cpu_temp_avg,
gpu_temp: metrics.temp.gpu_temp_avg,
ecpu_freq_mhz: metrics.ecpu_freq_mhz,
pcpu_freq_mhz: metrics.pcpu_freq_mhz,
gpu_freq_mhz: metrics.gpu_freq_mhz,
cpu_scaled_ratio: metrics.cpu_scaled_ratio,
gpu_scaled_ratio: metrics.gpu_scaled_ratio,
chip_name: soc.chip_name.clone(),
ecpu_cores: soc.ecpu_cores,
pcpu_cores: soc.pcpu_cores,
gpu_cores: soc.gpu_cores,
ram_total: metrics.memory.ram_total,
ram_usage: metrics.memory.ram_usage,
swap_total,
swap_used,
})
}
}
#[cfg(not(target_arch = "aarch64"))]
mod macmon_sampler {
pub struct MacmonData;
pub fn get_metrics() -> Option<MacmonData> { None }
}
pub use super::SystemStatsResponse;
async fn get_system_stats_handler() -> Response {
// 1. Try macmon first (Apple Silicon only)
if let Some(m) = macmon_sampler::get_metrics() {
let ram_total_mb = m.ram_total / 1024 / 1024;
let ram_usage_mb = m.ram_usage / 1024 / 1024;
let ram_available_mb = ram_total_mb.saturating_sub(ram_usage_mb);
let memory_used_percent = if ram_total_mb > 0 {
(ram_usage_mb as f64 / ram_total_mb as f64) * 100.0
} else {
0.0
};
let swap_total_mb = m.swap_total;
let swap_used_mb = m.swap_used;
let swap_available_mb = swap_total_mb.saturating_sub(swap_used_mb);
let swap_used_percent = if swap_total_mb > 0 {
(swap_used_mb as f64 / swap_total_mb as f64) * 100.0
} else {
0.0
};
let stats = SystemStatsResponse {
cpu_used_percent: m.cpu_scaled_ratio as f64 * 100.0,
memory_used_percent,
memory_available_mb: ram_available_mb,
memory_total_mb: ram_total_mb,
swap_used_percent,
swap_available_mb,
swap_total_mb,
gpu_available: true,
gpu_utilization: Some(m.gpu_scaled_ratio as f64 * 100.0),
gpu_memory_used_pct: None,
ane_available: m.ane_power > 0.0,
ane_utilization: None,
source: "macmon".to_string(),
chip_name: Some(m.chip_name),
cpu_power: Some(m.cpu_power as f64),
gpu_power: Some(m.gpu_power as f64),
ane_power: Some(m.ane_power as f64),
sys_power: Some(m.sys_power as f64),
ram_power: Some(m.ram_power as f64),
cpu_temp: Some(m.cpu_temp as f64),
gpu_temp: Some(m.gpu_temp as f64),
ecpu_freq_mhz: Some(m.ecpu_freq_mhz),
pcpu_freq_mhz: Some(m.pcpu_freq_mhz),
gpu_freq_mhz: Some(m.gpu_freq_mhz),
ecpu_cores: Some(m.ecpu_cores),
pcpu_cores: Some(m.pcpu_cores),
gpu_cores: Some(m.gpu_cores),
};
return axum::Json(stats).into_response();
}
// 2. Fallback: Use Core API
let client = reqwest::Client::new();
let url = format!("{}/health/detailed", CORE_API);
match client.get(&url).header("api_key", API_KEY).send().await {
Ok(resp) => {
match resp.json::<serde_json::Value>().await {
Ok(data) => {
if let Some(resources) = data.get("resources") {
let stats = SystemStatsResponse {
cpu_used_percent: resources.get("cpu_used_percent").and_then(|v| v.as_f64()).unwrap_or(0.0),
memory_used_percent: resources.get("memory_used_percent").and_then(|v| v.as_f64()).unwrap_or(0.0),
memory_available_mb: resources.get("memory_available_mb").and_then(|v| v.as_u64()).unwrap_or(0),
memory_total_mb: resources.get("memory_total_mb").and_then(|v| v.as_u64()).unwrap_or(0),
swap_used_percent: 0.0,
swap_available_mb: 0,
swap_total_mb: 0,
gpu_available: resources.get("gpu_available").and_then(|v| v.as_bool()).unwrap_or(false),
gpu_utilization: resources.get("gpu_utilization").and_then(|v| v.as_f64()),
gpu_memory_used_pct: resources.get("gpu_memory_used_pct").and_then(|v| v.as_f64()),
ane_available: resources.get("ane_available").and_then(|v| v.as_bool()).unwrap_or(false),
ane_utilization: resources.get("ane_utilization").and_then(|v| v.as_f64()),
source: "fallback".to_string(),
chip_name: None,
cpu_power: None,
gpu_power: None,
ane_power: None,
sys_power: None,
ram_power: None,
cpu_temp: None,
gpu_temp: None,
ecpu_freq_mhz: None,
pcpu_freq_mhz: None,
gpu_freq_mhz: None,
ecpu_cores: None,
pcpu_cores: None,
gpu_cores: None,
};
axum::Json(stats).into_response()
} else {
(StatusCode::INTERNAL_SERVER_ERROR, "No resources in response").into_response()
}
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(),
}
}
Err(e) => (StatusCode::BAD_GATEWAY, format!("Core API error: {}", e)).into_response(),
}
}
async fn validate_system_stats_handler(
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Response {
// Parse layer parameter
let layers: Vec<u32> = params
.get("layers")
.map(|s| s.split(',').filter_map(|n| n.trim().parse().ok()).collect())
.unwrap_or_else(|| vec![1, 2, 4]);
// Get current macmon stats
let stats = if let Some(m) = macmon_sampler::get_metrics() {
let ram_total_mb = m.ram_total / 1024 / 1024;
let ram_usage_mb = m.ram_usage / 1024 / 1024;
let ram_available_mb = ram_total_mb.saturating_sub(ram_usage_mb);
let memory_used_percent = if ram_total_mb > 0 {
(ram_usage_mb as f64 / ram_total_mb as f64) * 100.0
} else {
0.0
};
let swap_total_mb = m.swap_total;
let swap_used_mb = m.swap_used;
let swap_available_mb = swap_total_mb.saturating_sub(swap_used_mb);
let swap_used_percent = if swap_total_mb > 0 {
(swap_used_mb as f64 / swap_total_mb as f64) * 100.0
} else {
0.0
};
SystemStatsResponse {
cpu_used_percent: m.cpu_scaled_ratio as f64 * 100.0,
memory_used_percent,
memory_available_mb: ram_available_mb,
memory_total_mb: ram_total_mb,
swap_used_percent,
swap_available_mb,
swap_total_mb,
gpu_available: true,
gpu_utilization: Some(m.gpu_scaled_ratio as f64 * 100.0),
gpu_memory_used_pct: None,
ane_available: m.ane_power > 0.0,
ane_utilization: None,
source: "macmon".to_string(),
chip_name: Some(m.chip_name),
cpu_power: Some(m.cpu_power as f64),
gpu_power: Some(m.gpu_power as f64),
ane_power: Some(m.ane_power as f64),
sys_power: Some(m.sys_power as f64),
ram_power: Some(m.ram_power as f64),
cpu_temp: Some(m.cpu_temp as f64),
gpu_temp: Some(m.gpu_temp as f64),
ecpu_freq_mhz: Some(m.ecpu_freq_mhz),
pcpu_freq_mhz: Some(m.pcpu_freq_mhz),
gpu_freq_mhz: Some(m.gpu_freq_mhz),
ecpu_cores: Some(m.ecpu_cores),
pcpu_cores: Some(m.pcpu_cores),
gpu_cores: Some(m.gpu_cores),
}
} else {
return (StatusCode::INTERNAL_SERVER_ERROR, "macmon not available").into_response();
};
// Run validation
let result = super::validate::run_validation(&layers, &stats);
axum::Json(result).into_response()
}