Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.
transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
-> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
as immutable audit trail). Includes architecture-map references
to magnotia_core::*, magnotia_storage::*, etc. now pointing at
lumotia_*; dev-setup.md tracing output examples (lumotia_startup
target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
("lumotia_task_sync"); magnotia_locale i18n localStorage key
renamed + migration shim added; CSS keyframe names
magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
to "lumotia era" earlier — restored).
Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
legacy detection strings in legacy_and_target_paths() so the
migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
— migration call sites reference the legacy magnotia keys
deliberately.
- docs/handovers/ — historical audit trail.
cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
198 lines
5.7 KiB
Rust
198 lines
5.7 KiB
Rust
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) {
|
|
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::{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 Lumotia'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);
|
|
}
|
|
}
|