fix(kon): harden core crate — shared System instance, accurate CPU field, serialisable errors

- Share single System::new_all() in probe_system() instead of calling it twice
- Rename CpuInfo::core_count to logical_processors (sys.cpus().len() returns logical, not physical)
- Add fallback arm to probe_os() for unsupported cfg targets
- Add serde::Serialize to KonError for structured frontend error reporting
- Annotate dead code (ProviderRegistry, TranscriptMetadata) with #[allow(dead_code)] + TODO comments
- Update downstream references in recommendation tests and tauri hardware command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-17 00:19:04 +00:00
parent 4fa20255ae
commit 95c8d490c3
6 changed files with 60 additions and 49 deletions

View File

@@ -1,8 +1,14 @@
use std::path::PathBuf; use std::path::PathBuf;
use serde::Serialize;
use crate::types::ModelId; use crate::types::ModelId;
#[derive(Debug, thiserror::Error)] /// Structured error type for Kon.
///
/// Implements `Serialize` so errors can be sent to the frontend as
/// structured JSON rather than opaque strings.
#[derive(Debug, thiserror::Error, Serialize)]
pub enum KonError { pub enum KonError {
#[error("model not found: {0}")] #[error("model not found: {0}")]
ModelNotFound(ModelId), ModelNotFound(ModelId),
@@ -32,10 +38,23 @@ pub enum KonError {
StorageError(String), StorageError(String),
#[error("io error: {0}")] #[error("io error: {0}")]
Io(#[from] std::io::Error), Io(
#[from]
#[serde(serialize_with = "serialize_io_error")]
std::io::Error,
),
#[error("{0}")] #[error("{0}")]
Other(String), Other(String),
} }
/// Serialises `std::io::Error` as its display string, since it does
/// not implement `Serialize` natively.
fn serialize_io_error<S: serde::Serializer>(
err: &std::io::Error,
s: S,
) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&err.to_string())
}
pub type Result<T> = std::result::Result<T, KonError>; pub type Result<T> = std::result::Result<T, KonError>;

View File

@@ -13,7 +13,7 @@ pub struct SystemProfile {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CpuInfo { pub struct CpuInfo {
pub core_count: usize, pub logical_processors: usize,
pub brand: String, pub brand: String,
} }
@@ -49,16 +49,16 @@ pub enum Os {
Android, Android,
} }
pub fn probe_ram() -> Megabytes { /// Probes RAM from a shared `System` instance.
let sys = System::new_all(); fn probe_ram_from(sys: &System) -> Megabytes {
let total_bytes = sys.total_memory(); let total_bytes = sys.total_memory();
Megabytes(total_bytes / (1024 * 1024)) Megabytes(total_bytes / (1024 * 1024))
} }
pub fn probe_cpu() -> CpuInfo { /// Probes CPU info from a shared `System` instance.
let sys = System::new_all(); fn probe_cpu_from(sys: &System) -> CpuInfo {
CpuInfo { CpuInfo {
core_count: sys.cpus().len(), logical_processors: sys.cpus().len(),
brand: sys brand: sys
.cpus() .cpus()
.first() .first()
@@ -75,32 +75,30 @@ pub fn probe_gpu() -> Option<GpuInfo> {
pub fn probe_os() -> Os { pub fn probe_os() -> Os {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ return Os::Windows;
Os::Windows
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ return Os::Linux;
Os::Linux
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ return Os::MacOs;
Os::MacOs
}
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
{ return Os::Ios;
Os::Ios
}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ return Os::Android;
Os::Android
} // Fallback for unsupported targets — treat as Linux since
// most exotic/embedded targets are Unix-like.
#[allow(unreachable_code)]
Os::Linux
} }
/// Composes the individual probes. No logic here — just assembly. /// Composes the individual probes using a single `System` snapshot.
/// `System::new_all()` is expensive — calling it once rather than
/// per-probe avoids redundant OS queries.
pub fn probe_system() -> SystemProfile { pub fn probe_system() -> SystemProfile {
let sys = System::new_all();
SystemProfile { SystemProfile {
ram: probe_ram(), ram: probe_ram_from(&sys),
cpu: probe_cpu(), cpu: probe_cpu_from(&sys),
gpu: probe_gpu(), gpu: probe_gpu(),
os: probe_os(), os: probe_os(),
} }

View File

@@ -32,6 +32,8 @@ pub trait TextProcessor: Send + Sync {
/// Holds the active provider instances. Constructed at startup, /// Holds the active provider instances. Constructed at startup,
/// rebuilt when user changes provider in settings. /// rebuilt when user changes provider in settings.
// TODO: Wire into Tauri app state once multi-engine switching is implemented.
#[allow(dead_code)]
pub struct ProviderRegistry { pub struct ProviderRegistry {
pub stt: Arc<dyn SpeechToText>, pub stt: Arc<dyn SpeechToText>,
pub text: Option<Arc<dyn TextProcessor>>, pub text: Option<Arc<dyn TextProcessor>>,

View File

@@ -1,7 +1,5 @@
use crate::hardware::SystemProfile; use crate::hardware::SystemProfile;
use crate::model_registry::{ use crate::model_registry::{all_models, AccuracyTier, Engine, ModelEntry, SpeedTier};
all_models, AccuracyTier, Engine, ModelEntry, SpeedTier,
};
use crate::types::Megabytes; use crate::types::Megabytes;
/// A model's suitability score for a given system. Higher is better. /// A model's suitability score for a given system. Higher is better.
@@ -14,10 +12,7 @@ pub struct ScoredModel {
/// Scores a single model against a system profile. /// Scores a single model against a system profile.
/// Pure function, no side effects. /// Pure function, no side effects.
pub fn score_model( pub fn score_model(model: &'static ModelEntry, profile: &SystemProfile) -> Option<ScoredModel> {
model: &'static ModelEntry,
profile: &SystemProfile,
) -> Option<ScoredModel> {
if model.ram_required > profile.ram { if model.ram_required > profile.ram {
return None; return None;
} }
@@ -41,9 +36,7 @@ pub fn score_model(
if let Some(gpu) = &profile.gpu { if let Some(gpu) = &profile.gpu {
let has_accel = match model.engine { let has_accel = match model.engine {
Engine::Whisper => { Engine::Whisper => {
gpu.acceleration.metal gpu.acceleration.metal || gpu.acceleration.vulkan || gpu.acceleration.cuda
|| gpu.acceleration.vulkan
|| gpu.acceleration.cuda
} }
Engine::Parakeet | Engine::Moonshine => { Engine::Parakeet | Engine::Moonshine => {
gpu.acceleration.cuda || gpu.acceleration.vulkan gpu.acceleration.cuda || gpu.acceleration.vulkan
@@ -55,8 +48,7 @@ pub fn score_model(
} }
} }
let headroom = let headroom = Megabytes(profile.ram.0.saturating_sub(model.ram_required.0));
Megabytes(profile.ram.0.saturating_sub(model.ram_required.0));
if headroom > Megabytes::from_gb(4.0) { if headroom > Megabytes::from_gb(4.0) {
score += 10.0; score += 10.0;
} }
@@ -99,7 +91,7 @@ mod tests {
SystemProfile { SystemProfile {
ram, ram,
cpu: CpuInfo { cpu: CpuInfo {
core_count: 8, logical_processors: 8,
brand: "Test CPU".into(), brand: "Test CPU".into(),
}, },
gpu: None, gpu: None,
@@ -111,7 +103,7 @@ mod tests {
SystemProfile { SystemProfile {
ram, ram,
cpu: CpuInfo { cpu: CpuInfo {
core_count: 8, logical_processors: 8,
brand: "Test CPU".into(), brand: "Test CPU".into(),
}, },
gpu: Some(GpuInfo { gpu: Some(GpuInfo {
@@ -157,14 +149,12 @@ mod tests {
.find(|m| m.engine == Engine::Parakeet) .find(|m| m.engine == Engine::Parakeet)
.expect("need a Parakeet model"); .expect("need a Parakeet model");
let gpu_score = let gpu_score = score_model(model, &profile_with_gpu(Megabytes(16384)))
score_model(model, &profile_with_gpu(Megabytes(16384))) .unwrap()
.unwrap() .score;
.score; let cpu_score = score_model(model, &profile_with_ram(Megabytes(16384)))
let cpu_score = .unwrap()
score_model(model, &profile_with_ram(Megabytes(16384))) .score;
.unwrap()
.score;
assert!(gpu_score > cpu_score); assert!(gpu_score > cpu_score);
} }

View File

@@ -168,6 +168,8 @@ pub struct TranscriptionOptions {
/// Full provenance metadata for a transcript. /// Full provenance metadata for a transcript.
/// Captures everything needed to reproduce the transcription. /// Captures everything needed to reproduce the transcription.
// TODO: Attach to Transcript once the store layer persists transcription provenance.
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptMetadata { pub struct TranscriptMetadata {
pub engine: String, pub engine: String,

View File

@@ -31,7 +31,7 @@ pub fn probe_system() -> Result<SystemInfo, String> {
Ok(SystemInfo { Ok(SystemInfo {
ram_mb: profile.ram.0, ram_mb: profile.ram.0,
cpu_brand: profile.cpu.brand, cpu_brand: profile.cpu.brand,
cpu_cores: profile.cpu.core_count, cpu_cores: profile.cpu.logical_processors,
os: match profile.os { os: match profile.os {
Os::Windows => "Windows", Os::Windows => "Windows",
Os::Linux => "Linux", Os::Linux => "Linux",