use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize)] pub struct ModelInfo { pub name: String, pub path: String, pub size_bytes: u64, pub kind: String, } fn huggingface_cache_dir() -> PathBuf { // Follows huggingface_hub default cache location if let Ok(custom) = std::env::var("HF_HOME") { return PathBuf::from(custom).join("hub"); } if let Ok(custom) = std::env::var("XDG_CACHE_HOME") { return PathBuf::from(custom).join("huggingface").join("hub"); } dirs::home_dir() .unwrap_or_default() .join(".cache") .join("huggingface") .join("hub") } fn scan_whisper_models() -> Vec { let cache_dir = huggingface_cache_dir(); if !cache_dir.exists() { return vec![]; } let mut models = vec![]; let pattern = "models--Systran--faster-whisper-"; let Ok(entries) = std::fs::read_dir(&cache_dir) else { return vec![]; }; for entry in entries.flatten() { let name = entry.file_name(); let name_str = name.to_string_lossy(); if !name_str.starts_with(pattern) { continue; } let model_name = name_str .strip_prefix(pattern) .unwrap_or(&name_str) .to_string(); // The actual model files are in snapshots/ subdirectory let snapshots_dir = entry.path().join("snapshots"); let mut total_size = 0u64; if let Ok(snap_entries) = std::fs::read_dir(&snapshots_dir) { for snap in snap_entries.flatten() { total_size += dir_size(&snap.path()); } } // If no snapshots dir, try blobs/ if total_size == 0 { let blobs_dir = entry.path().join("blobs"); if blobs_dir.exists() { total_size = dir_size(&blobs_dir); } } models.push(ModelInfo { name: model_name, path: entry.path().to_string_lossy().to_string(), size_bytes: total_size, kind: "whisper".to_string(), }); } models } fn scan_llm_models(app_data_dir: &PathBuf) -> Vec { let models_dir = app_data_dir.join("models"); if !models_dir.exists() { return vec![]; } let mut models = vec![]; let Ok(entries) = std::fs::read_dir(&models_dir) else { return vec![]; }; for entry in entries.flatten() { let path = entry.path(); if path.extension().map(|e| e == "gguf").unwrap_or(false) { let meta = std::fs::metadata(&path).ok(); models.push(ModelInfo { name: entry.file_name().to_string_lossy().to_string(), path: path.to_string_lossy().to_string(), size_bytes: meta.map(|m| m.len()).unwrap_or(0), kind: "llm".to_string(), }); } } models } fn dir_size(path: &std::path::Path) -> u64 { let mut total = 0u64; if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { total += dir_size(&path); } else if let Ok(meta) = std::fs::metadata(&path) { total += meta.len(); } } } total } pub fn list_models(app_data_dir: &PathBuf) -> Vec { let mut models = scan_whisper_models(); models.extend(scan_llm_models(app_data_dir)); models } pub fn delete_model(path: &str) -> Result<(), String> { let path = std::path::Path::new(path); if !path.exists() { return Err("Model path not found".to_string()); } if path.is_dir() { std::fs::remove_dir_all(path) .map_err(|e| format!("Failed to delete model: {e}"))?; } else { std::fs::remove_file(path) .map_err(|e| format!("Failed to delete model: {e}"))?; } Ok(()) }