use crate::hardware::SystemProfile; use crate::model_registry::{all_models, AccuracyTier, Engine, ModelEntry, SpeedTier}; use crate::types::Megabytes; /// A model's suitability score for a given system. Higher is better. /// No boolean flags — position in the ranked list conveys recommendation. pub struct ScoredModel { pub entry: &'static ModelEntry, pub score: f64, pub reason: String, } /// Scores a single model against a system profile. /// Pure function, no side effects. pub fn score_model(model: &'static ModelEntry, profile: &SystemProfile) -> Option { if model.ram_required > profile.ram { return None; } let mut score = 0.0; let mut reasons: Vec = Vec::new(); score += match model.speed_tier { SpeedTier::Instant => 40.0, SpeedTier::Fast => 30.0, SpeedTier::Moderate => 20.0, SpeedTier::Slow => 10.0, }; score += match model.accuracy_tier { AccuracyTier::Excellent => 30.0, AccuracyTier::Great => 20.0, AccuracyTier::Good => 10.0, }; if let Some(gpu) = &profile.gpu { let has_accel = match model.engine { Engine::Whisper => { gpu.acceleration.metal || gpu.acceleration.vulkan || gpu.acceleration.cuda } Engine::Parakeet | Engine::Moonshine => { gpu.acceleration.cuda || gpu.acceleration.vulkan } }; if has_accel { score += 15.0; reasons.push("GPU accelerated on your system".into()); } } let headroom = Megabytes(profile.ram.0.saturating_sub(model.ram_required.0)); if headroom > Megabytes::from_gb(4.0) { score += 10.0; } let reason = if reasons.is_empty() { model.description.to_string() } else { reasons.join(". ") }; Some(ScoredModel { entry: model, score, reason, }) } /// Scores all models and returns them ranked. /// Index 0 is the recommendation. No flag arguments. pub fn rank_recommendations(profile: &SystemProfile) -> Vec { let mut scored: Vec = all_models() .iter() .filter_map(|model| score_model(model, profile)) .collect(); scored.sort_by(|a, b| { b.score .partial_cmp(&a.score) .unwrap_or(std::cmp::Ordering::Equal) }); scored } #[cfg(test)] mod tests { use super::*; use crate::hardware::{CpuFeatures, CpuInfo, GpuAcceleration, GpuInfo, GpuVendor, Os}; fn profile_with_ram(ram: Megabytes) -> SystemProfile { SystemProfile { ram, cpu: CpuInfo { logical_processors: 8, brand: "Test CPU".into(), features: CpuFeatures::default(), }, gpu: None, os: Os::Windows, } } fn profile_with_gpu(ram: Megabytes) -> SystemProfile { SystemProfile { ram, cpu: CpuInfo { logical_processors: 8, brand: "Test CPU".into(), features: CpuFeatures::default(), }, gpu: Some(GpuInfo { vendor: GpuVendor::Nvidia, vram: Megabytes(8192), acceleration: GpuAcceleration { cuda: true, metal: false, vulkan: true, }, }), os: Os::Windows, } } #[test] fn score_model_excludes_models_exceeding_available_ram() { let profile = profile_with_ram(Megabytes(256)); let model = all_models() .iter() .find(|m| m.ram_required > Megabytes(256)) .expect("need a model larger than 256 MB"); let result = score_model(model, &profile); assert!(result.is_none()); } #[test] fn score_model_includes_models_fitting_in_ram() { let profile = profile_with_ram(Megabytes(16384)); let model = &all_models()[0]; let result = score_model(model, &profile); assert!(result.is_some()); } #[test] fn score_model_boosts_gpu_accelerated_models() { let model = all_models() .iter() .find(|m| m.engine == Engine::Parakeet) .expect("need a Parakeet model"); let gpu_score = score_model(model, &profile_with_gpu(Megabytes(16384))) .unwrap() .score; let cpu_score = score_model(model, &profile_with_ram(Megabytes(16384))) .unwrap() .score; assert!(gpu_score > cpu_score); } #[test] fn rank_recommendations_places_highest_score_first() { let profile = profile_with_ram(Megabytes(16384)); let ranked = rank_recommendations(&profile); assert!(ranked.len() >= 2); assert!(ranked[0].score >= ranked[1].score); } #[test] fn rank_recommendations_returns_empty_for_very_low_ram() { let profile = profile_with_ram(Megabytes(128)); let ranked = rank_recommendations(&profile); assert!(ranked.is_empty()); } #[test] fn parakeet_is_top_recommendation_when_hardware_supports_it() { // Any machine that fits Parakeet in RAM should see it ranked first — // Parakeet-TDT is English-only but beats Whisper on English at lower // latency, so it's Magnotia's default recommendation when eligible. // (Users on non-English languages adjust manually — handled at the // settings-UI level, not at the scoring level for now.) let profile = profile_with_ram(Megabytes(16384)); let ranked = rank_recommendations(&profile); let top = ranked.first().expect("at least one model ranks"); assert_eq!(top.entry.engine, Engine::Parakeet); } }