feat(kon): add core crate — types, traits, hardware, model registry, recommendation
- Value objects: ModelId, EngineName, Megabytes, AudioSamples, Segment, Transcript - KonError enum with thiserror - Constants centralised: audio pipeline, VAD, RAM thresholds, inference threading - SpeechToText and TextProcessor provider traits with ProviderRegistry - Unified model registry (Whisper tiny/base/small/medium + Parakeet CTC int8) - Hardware detection: probe_ram, probe_cpu, probe_gpu (stub), probe_os - Recommendation engine: score_model (pure function), rank_recommendations (sorted) - 5 tests passing, clippy clean Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
190
crates/core/src/recommendation.rs
Normal file
190
crates/core/src/recommendation.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
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<ScoredModel> {
|
||||
if model.ram_required > profile.ram {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut score = 0.0;
|
||||
let mut reasons: Vec<String> = 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<ScoredModel> {
|
||||
let mut scored: Vec<ScoredModel> = 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::{CpuInfo, GpuAcceleration, GpuInfo, GpuVendor, Os};
|
||||
|
||||
fn profile_with_ram(ram: Megabytes) -> SystemProfile {
|
||||
SystemProfile {
|
||||
ram,
|
||||
cpu: CpuInfo {
|
||||
core_count: 8,
|
||||
brand: "Test CPU".into(),
|
||||
},
|
||||
gpu: None,
|
||||
os: Os::Windows,
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_with_gpu(ram: Megabytes) -> SystemProfile {
|
||||
SystemProfile {
|
||||
ram,
|
||||
cpu: CpuInfo {
|
||||
core_count: 8,
|
||||
brand: "Test CPU".into(),
|
||||
},
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user