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:
@@ -5,3 +5,8 @@ edition = "2021"
|
||||
description = "Core types, constants, traits, hardware detection, and model registry for Kon"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
sysinfo = "0.35"
|
||||
async-trait = "0.1"
|
||||
|
||||
49
crates/core/src/constants.rs
Normal file
49
crates/core/src/constants.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
/// Audio pipeline constants.
|
||||
pub const WHISPER_SAMPLE_RATE: u32 = 16_000;
|
||||
pub const WHISPER_CHANNELS: u16 = 1;
|
||||
|
||||
/// Parakeet mel spectrogram constants.
|
||||
pub const PARAKEET_N_FFT: usize = 512;
|
||||
pub const PARAKEET_HOP_LENGTH: usize = 160;
|
||||
pub const PARAKEET_WIN_LENGTH: usize = 400;
|
||||
pub const PARAKEET_N_MELS: usize = 80;
|
||||
pub const PARAKEET_PRE_EMPHASIS: f32 = 0.97;
|
||||
pub const PARAKEET_BLANK_TOKEN: usize = 1024;
|
||||
pub const PARAKEET_LOG_GUARD: f32 = 5.960_464_5e-8; // 2^-24
|
||||
|
||||
/// Chunk timing for live transcription.
|
||||
pub const CHUNK_INTERVAL_MS: u64 = 3000;
|
||||
pub const MIN_CHUNK_SAMPLES: usize = 8000;
|
||||
|
||||
/// Post-processing thresholds.
|
||||
pub const SMART_PARAGRAPH_GAP_SECS: f64 = 2.0;
|
||||
|
||||
/// Thread count for inference. Leaves headroom for the UI thread.
|
||||
pub const MIN_INFERENCE_THREADS: usize = 4;
|
||||
|
||||
/// History limits.
|
||||
pub const HISTORY_MAX_ENTRIES: usize = 100;
|
||||
|
||||
/// RAM thresholds for model recommendations (in GB).
|
||||
pub const RAM_MINIMUM_FOR_LOCAL_STT: f64 = 2.0;
|
||||
pub const RAM_THRESHOLD_LIGHTWEIGHT: f64 = 4.0;
|
||||
pub const RAM_THRESHOLD_STANDARD: f64 = 8.0;
|
||||
pub const RAM_THRESHOLD_COMFORTABLE: f64 = 16.0;
|
||||
|
||||
/// VAD configuration defaults.
|
||||
pub const VAD_SPEECH_THRESHOLD: f64 = 0.5;
|
||||
pub const VAD_MIN_SPEECH_DURATION_MS: u32 = 250;
|
||||
pub const VAD_MAX_SPEECH_DURATION_S: u32 = 30;
|
||||
pub const VAD_MIN_SILENCE_DURATION_MS: u32 = 300;
|
||||
pub const VAD_SPEECH_PAD_MS: u32 = 100;
|
||||
|
||||
/// Model download chunk size for progress reporting.
|
||||
pub const DOWNLOAD_CHUNK_BYTES: usize = 65_536;
|
||||
|
||||
/// Inference thread count based on available parallelism.
|
||||
pub fn inference_thread_count() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|p| p.get().saturating_sub(1))
|
||||
.unwrap_or(MIN_INFERENCE_THREADS)
|
||||
.max(MIN_INFERENCE_THREADS)
|
||||
}
|
||||
41
crates/core/src/error.rs
Normal file
41
crates/core/src/error.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::types::ModelId;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KonError {
|
||||
#[error("model not found: {0}")]
|
||||
ModelNotFound(ModelId),
|
||||
|
||||
#[error("model not downloaded: {0}")]
|
||||
ModelNotDownloaded(ModelId),
|
||||
|
||||
#[error("engine not loaded: call load_model first")]
|
||||
EngineNotLoaded,
|
||||
|
||||
#[error("transcription failed: {0}")]
|
||||
TranscriptionFailed(String),
|
||||
|
||||
#[error("audio decode failed: {0}")]
|
||||
AudioDecodeFailed(String),
|
||||
|
||||
#[error("audio capture failed: {0}")]
|
||||
AudioCaptureFailed(String),
|
||||
|
||||
#[error("model download failed: {0}")]
|
||||
DownloadFailed(String),
|
||||
|
||||
#[error("file not found: {}", .0.display())]
|
||||
FileNotFound(PathBuf),
|
||||
|
||||
#[error("storage error: {0}")]
|
||||
StorageError(String),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KonError>;
|
||||
107
crates/core/src/hardware.rs
Normal file
107
crates/core/src/hardware.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use sysinfo::System;
|
||||
|
||||
use crate::types::Megabytes;
|
||||
|
||||
/// Detected system capabilities.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SystemProfile {
|
||||
pub ram: Megabytes,
|
||||
pub cpu: CpuInfo,
|
||||
pub gpu: Option<GpuInfo>,
|
||||
pub os: Os,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CpuInfo {
|
||||
pub core_count: usize,
|
||||
pub brand: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GpuInfo {
|
||||
pub vendor: GpuVendor,
|
||||
pub vram: Megabytes,
|
||||
pub acceleration: GpuAcceleration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GpuVendor {
|
||||
Nvidia,
|
||||
Amd,
|
||||
Intel,
|
||||
Apple,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GpuAcceleration {
|
||||
pub cuda: bool,
|
||||
pub metal: bool,
|
||||
pub vulkan: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Os {
|
||||
Windows,
|
||||
Linux,
|
||||
MacOs,
|
||||
Ios,
|
||||
Android,
|
||||
}
|
||||
|
||||
pub fn probe_ram() -> Megabytes {
|
||||
let sys = System::new_all();
|
||||
let total_bytes = sys.total_memory();
|
||||
Megabytes(total_bytes / (1024 * 1024))
|
||||
}
|
||||
|
||||
pub fn probe_cpu() -> CpuInfo {
|
||||
let sys = System::new_all();
|
||||
CpuInfo {
|
||||
core_count: sys.cpus().len(),
|
||||
brand: sys
|
||||
.cpus()
|
||||
.first()
|
||||
.map(|c| c.brand().to_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn probe_gpu() -> Option<GpuInfo> {
|
||||
// GPU detection via wgpu or platform-specific APIs.
|
||||
// Placeholder: returns None until wgpu or nvml integration is added.
|
||||
None
|
||||
}
|
||||
|
||||
pub fn probe_os() -> Os {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Os::Windows
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Os::Linux
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Os::MacOs
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
Os::Ios
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Os::Android
|
||||
}
|
||||
}
|
||||
|
||||
/// Composes the individual probes. No logic here — just assembly.
|
||||
pub fn probe_system() -> SystemProfile {
|
||||
SystemProfile {
|
||||
ram: probe_ram(),
|
||||
cpu: probe_cpu(),
|
||||
gpu: probe_gpu(),
|
||||
os: probe_os(),
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,13 @@
|
||||
// kon-core: Foundation types, constants, provider traits, hardware detection,
|
||||
// model registry, and recommendation engine.
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod hardware;
|
||||
pub mod model_registry;
|
||||
pub mod providers;
|
||||
pub mod recommendation;
|
||||
pub mod types;
|
||||
|
||||
pub use error::{KonError, Result};
|
||||
pub use types::{
|
||||
AudioSamples, DownloadProgress, EngineName, Megabytes, ModelId, Segment,
|
||||
Transcript, TranscriptionOptions,
|
||||
};
|
||||
|
||||
166
crates/core/src/model_registry.rs
Normal file
166
crates/core/src/model_registry.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::types::{Megabytes, ModelId};
|
||||
|
||||
/// Which inference backend a model uses.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Engine {
|
||||
Whisper,
|
||||
Parakeet,
|
||||
Moonshine,
|
||||
}
|
||||
|
||||
/// Qualitative speed classification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SpeedTier {
|
||||
Instant,
|
||||
Fast,
|
||||
Moderate,
|
||||
Slow,
|
||||
}
|
||||
|
||||
/// Qualitative accuracy classification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccuracyTier {
|
||||
Excellent,
|
||||
Great,
|
||||
Good,
|
||||
}
|
||||
|
||||
/// Language support scope.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LanguageSupport {
|
||||
EnglishOnly,
|
||||
Multilingual(u16),
|
||||
}
|
||||
|
||||
/// File required for a model download.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelFile {
|
||||
pub filename: &'static str,
|
||||
pub url: &'static str,
|
||||
pub size: Megabytes,
|
||||
}
|
||||
|
||||
/// All metadata for a single downloadable model.
|
||||
/// This is pure data — no scoring logic lives here.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelEntry {
|
||||
pub id: ModelId,
|
||||
pub engine: Engine,
|
||||
pub display_name: &'static str,
|
||||
pub disk_size: Megabytes,
|
||||
pub ram_required: Megabytes,
|
||||
pub speed_tier: SpeedTier,
|
||||
pub accuracy_tier: AccuracyTier,
|
||||
pub languages: LanguageSupport,
|
||||
pub files: Vec<ModelFile>,
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
static ALL_MODELS: LazyLock<Vec<ModelEntry>> = LazyLock::new(|| {
|
||||
vec![
|
||||
ModelEntry {
|
||||
id: ModelId::new("parakeet-ctc-0.6b-int8"),
|
||||
engine: Engine::Parakeet,
|
||||
display_name: "Parakeet CTC 0.6B (int8)",
|
||||
disk_size: Megabytes(613),
|
||||
ram_required: Megabytes(600),
|
||||
speed_tier: SpeedTier::Instant,
|
||||
accuracy_tier: AccuracyTier::Great,
|
||||
languages: LanguageSupport::EnglishOnly,
|
||||
files: vec![
|
||||
ModelFile {
|
||||
filename: "model_int8.onnx",
|
||||
url: "https://huggingface.co/onnx-community/parakeet-ctc-0.6b-ONNX/resolve/main/onnx/model_int8.onnx",
|
||||
size: Megabytes(1),
|
||||
},
|
||||
ModelFile {
|
||||
filename: "model_int8.onnx_data",
|
||||
url: "https://huggingface.co/onnx-community/parakeet-ctc-0.6b-ONNX/resolve/main/onnx/model_int8.onnx_data",
|
||||
size: Megabytes(611),
|
||||
},
|
||||
ModelFile {
|
||||
filename: "tokenizer.json",
|
||||
url: "https://huggingface.co/onnx-community/parakeet-ctc-0.6b-ONNX/resolve/main/tokenizer.json",
|
||||
size: Megabytes(1),
|
||||
},
|
||||
],
|
||||
description: "Fastest local model — near-instant transcription",
|
||||
},
|
||||
ModelEntry {
|
||||
id: ModelId::new("whisper-tiny-en"),
|
||||
engine: Engine::Whisper,
|
||||
display_name: "Whisper Tiny (English)",
|
||||
disk_size: Megabytes(75),
|
||||
ram_required: Megabytes(390),
|
||||
speed_tier: SpeedTier::Fast,
|
||||
accuracy_tier: AccuracyTier::Good,
|
||||
languages: LanguageSupport::EnglishOnly,
|
||||
files: vec![ModelFile {
|
||||
filename: "ggml-tiny.en.bin",
|
||||
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin",
|
||||
size: Megabytes(75),
|
||||
}],
|
||||
description: "Bundled with app — works instantly",
|
||||
},
|
||||
ModelEntry {
|
||||
id: ModelId::new("whisper-base-en"),
|
||||
engine: Engine::Whisper,
|
||||
display_name: "Whisper Base (English)",
|
||||
disk_size: Megabytes(142),
|
||||
ram_required: Megabytes(500),
|
||||
speed_tier: SpeedTier::Fast,
|
||||
accuracy_tier: AccuracyTier::Good,
|
||||
languages: LanguageSupport::EnglishOnly,
|
||||
files: vec![ModelFile {
|
||||
filename: "ggml-base.en.bin",
|
||||
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin",
|
||||
size: Megabytes(142),
|
||||
}],
|
||||
description: "Good balance of speed and accuracy",
|
||||
},
|
||||
ModelEntry {
|
||||
id: ModelId::new("whisper-small-en"),
|
||||
engine: Engine::Whisper,
|
||||
display_name: "Whisper Small (English)",
|
||||
disk_size: Megabytes(466),
|
||||
ram_required: Megabytes(1024),
|
||||
speed_tier: SpeedTier::Moderate,
|
||||
accuracy_tier: AccuracyTier::Great,
|
||||
languages: LanguageSupport::EnglishOnly,
|
||||
files: vec![ModelFile {
|
||||
filename: "ggml-small.en.bin",
|
||||
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin",
|
||||
size: Megabytes(466),
|
||||
}],
|
||||
description: "Accuracy-first English transcription",
|
||||
},
|
||||
ModelEntry {
|
||||
id: ModelId::new("whisper-medium-en"),
|
||||
engine: Engine::Whisper,
|
||||
display_name: "Whisper Medium (English)",
|
||||
disk_size: Megabytes(1500),
|
||||
ram_required: Megabytes(2600),
|
||||
speed_tier: SpeedTier::Slow,
|
||||
accuracy_tier: AccuracyTier::Excellent,
|
||||
languages: LanguageSupport::EnglishOnly,
|
||||
files: vec![ModelFile {
|
||||
filename: "ggml-medium.en.bin",
|
||||
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.en.bin",
|
||||
size: Megabytes(1500),
|
||||
}],
|
||||
description: "Best Whisper accuracy — needs 4+ GB RAM",
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
/// Returns all known models. Pure data, no scoring.
|
||||
pub fn all_models() -> &'static [ModelEntry] {
|
||||
&ALL_MODELS
|
||||
}
|
||||
|
||||
/// Find a model by its ID.
|
||||
pub fn find_model(id: &ModelId) -> Option<&'static ModelEntry> {
|
||||
ALL_MODELS.iter().find(|m| &m.id == id)
|
||||
}
|
||||
38
crates/core/src/providers.rs
Normal file
38
crates/core/src/providers.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::types::{AudioSamples, EngineName, Transcript, TranscriptionOptions};
|
||||
|
||||
/// Any speech-to-text engine implements this trait.
|
||||
/// Base types know nothing about their derivatives.
|
||||
#[async_trait]
|
||||
pub trait SpeechToText: Send + Sync {
|
||||
async fn transcribe(
|
||||
&self,
|
||||
audio: AudioSamples,
|
||||
options: &TranscriptionOptions,
|
||||
) -> Result<Transcript>;
|
||||
|
||||
fn name(&self) -> &EngineName;
|
||||
|
||||
fn is_available(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Any text post-processor implements this trait.
|
||||
#[async_trait]
|
||||
pub trait TextProcessor: Send + Sync {
|
||||
async fn process(&self, text: &str, instruction: &str) -> Result<String>;
|
||||
|
||||
fn name(&self) -> &EngineName;
|
||||
|
||||
fn is_available(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Holds the active provider instances. Constructed at startup,
|
||||
/// rebuilt when user changes provider in settings.
|
||||
pub struct ProviderRegistry {
|
||||
pub stt: Arc<dyn SpeechToText>,
|
||||
pub text: Option<Arc<dyn TextProcessor>>,
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
177
crates/core/src/types.rs
Normal file
177
crates/core/src/types.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Prevents passing raw strings where model IDs are expected.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ModelId(String);
|
||||
|
||||
impl ModelId {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevents passing raw strings where engine names are expected.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EngineName(String);
|
||||
|
||||
impl EngineName {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self(name.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EngineName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevents mixing up bytes, megabytes, and gigabytes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Megabytes(pub u64);
|
||||
|
||||
impl Megabytes {
|
||||
pub fn from_gb(gb: f64) -> Self {
|
||||
Self((gb * 1024.0) as u64)
|
||||
}
|
||||
|
||||
pub fn as_gb(&self) -> f64 {
|
||||
self.0 as f64 / 1024.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Megabytes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.0 >= 1024 {
|
||||
write!(f, "{:.1} GB", self.as_gb())
|
||||
} else {
|
||||
write!(f, "{} MB", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps raw audio samples with metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioSamples {
|
||||
samples: Vec<f32>,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
impl AudioSamples {
|
||||
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16) -> Self {
|
||||
Self {
|
||||
samples,
|
||||
sample_rate,
|
||||
channels,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mono_16khz(samples: Vec<f32>) -> Self {
|
||||
Self {
|
||||
samples,
|
||||
sample_rate: crate::constants::WHISPER_SAMPLE_RATE,
|
||||
channels: crate::constants::WHISPER_CHANNELS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn samples(&self) -> &[f32] {
|
||||
&self.samples
|
||||
}
|
||||
|
||||
pub fn into_samples(self) -> Vec<f32> {
|
||||
self.samples
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> u16 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
pub fn duration_secs(&self) -> f64 {
|
||||
if self.sample_rate == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.samples.len() as f64 / self.sample_rate as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// A single timed segment of a transcription.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Segment {
|
||||
pub start: f64,
|
||||
pub end: f64,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// The result of a transcription.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Transcript {
|
||||
segments: Vec<Segment>,
|
||||
language: String,
|
||||
duration: f64,
|
||||
}
|
||||
|
||||
impl Transcript {
|
||||
pub fn new(segments: Vec<Segment>, language: String, duration: f64) -> Self {
|
||||
Self {
|
||||
segments,
|
||||
language,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
self.segments
|
||||
.iter()
|
||||
.map(|s| s.text.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn segments(&self) -> &[Segment] {
|
||||
&self.segments
|
||||
}
|
||||
|
||||
pub fn language(&self) -> &str {
|
||||
&self.language
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> f64 {
|
||||
self.duration
|
||||
}
|
||||
}
|
||||
|
||||
/// Options passed to a transcription engine.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TranscriptionOptions {
|
||||
pub language: Option<String>,
|
||||
pub initial_prompt: Option<String>,
|
||||
}
|
||||
|
||||
/// Progress update during model download.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DownloadProgress {
|
||||
pub model_id: ModelId,
|
||||
pub file_name: String,
|
||||
pub bytes_downloaded: u64,
|
||||
pub total_bytes: u64,
|
||||
pub percent: u8,
|
||||
}
|
||||
Reference in New Issue
Block a user