mod cache;
use std::fmt;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use crate::{Error, Result};
pub const RETRIES: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModelInfo {
pub name: &'static str,
pub full_name: &'static str,
pub size: u64,
pub languages: &'static str,
pub punctuated: bool,
}
impl ModelInfo {
pub fn repo(&self) -> String {
format!("handy-computer/{}-gguf", self.full_name)
}
pub fn file(&self) -> String {
format!("{}-Q8_0.gguf", self.full_name)
}
}
#[rustfmt::skip]
pub static MODELS: &[ModelInfo] = &[
ModelInfo { name: "parakeet-v3", full_name: "parakeet-tdt-0.6b-v3", size: 739_508_576, languages: "25 European languages", punctuated: true },
ModelInfo { name: "parakeet-v2", full_name: "parakeet-tdt-0.6b-v2", size: 729_574_912, languages: "English", punctuated: true },
ModelInfo { name: "gigaam-v3", full_name: "gigaam-v3-e2e-rnnt", size: 273_724_832, languages: "Russian", punctuated: true },
ModelInfo { name: "gigaam-v3-e2e-ctc", full_name: "gigaam-v3-e2e-ctc", size: 272_151_136, languages: "Russian", punctuated: true },
ModelInfo { name: "gigaam-v3-rnnt", full_name: "gigaam-v3-rnnt", size: 273_022_880, languages: "Russian", punctuated: false },
ModelInfo { name: "gigaam-v3-ctc", full_name: "gigaam-v3-ctc", size: 271_803_328, languages: "Russian", punctuated: false },
ModelInfo { name: "whisper-turbo", full_name: "whisper-large-v3-turbo", size: 886_381_760, languages: "100 languages", punctuated: true },
ModelInfo { name: "whisper-tiny", full_name: "whisper-tiny", size: 45_981_088, languages: "99 languages", punctuated: true },
ModelInfo { name: "whisper-base", full_name: "whisper-base", size: 84_962_880, languages: "99 languages", punctuated: true },
ModelInfo { name: "whisper-small", full_name: "whisper-small", size: 269_751_136, languages: "99 languages", punctuated: true },
ModelInfo { name: "whisper-medium", full_name: "whisper-medium", size: 831_538_144, languages: "99 languages", punctuated: true },
ModelInfo { name: "whisper-large-v3", full_name: "whisper-large-v3", size: 1_668_741_440, languages: "100 languages", punctuated: true },
ModelInfo { name: "whisper-tiny.en", full_name: "whisper-tiny.en", size: 45_904_544, languages: "English", punctuated: true },
ModelInfo { name: "whisper-base.en", full_name: "whisper-base.en", size: 84_886_208, languages: "English", punctuated: true },
ModelInfo { name: "whisper-small.en", full_name: "whisper-small.en", size: 269_674_144, languages: "English", punctuated: true },
ModelInfo { name: "whisper-medium.en", full_name: "whisper-medium.en", size: 831_460_928, languages: "English", punctuated: true },
ModelInfo { name: "whisper-large", full_name: "whisper-large", size: 1_667_964_192, languages: "99 languages", punctuated: true },
ModelInfo { name: "whisper-large-v2", full_name: "whisper-large-v2", size: 1_667_964_224, languages: "99 languages", punctuated: true },
ModelInfo { name: "breeze-asr-25", full_name: "Breeze-ASR-25", size: 1_667_964_224, languages: "Taiwanese Mandarin and English", punctuated: true },
ModelInfo { name: "moonshine-tiny", full_name: "moonshine-tiny", size: 35_466_912, languages: "English", punctuated: true },
ModelInfo { name: "moonshine-base", full_name: "moonshine-base", size: 77_476_480, languages: "English", punctuated: true },
];
pub fn lookup(name: &str) -> Result<&'static ModelInfo> {
if let Some(m) = MODELS.iter().find(|m| m.name == name || m.full_name == name) {
return Ok(m);
}
let closest = (MODELS.iter().flat_map(|m| [m.name, m.full_name]))
.map(|n| (distance(name, n), n))
.min()
.filter(|&(d, n)| d <= (n.len() / 3).max(2));
Err(Error::UnknownModel { name: name.into(), suggestion: closest.map(|(_, n)| n) })
}
pub(crate) fn hint(suggestion: Option<&str>) -> String {
suggestion.map_or(".".into(), |n| format!(", did you mean {n}?"))
}
pub(crate) fn names() -> String {
MODELS.iter().map(|m| m.name).collect::<Vec<_>>().join(", ")
}
fn distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut diag = row[0];
row[0] = i + 1;
for (j, &cb) in b.iter().enumerate() {
let next = (diag + (ca != cb) as usize).min(row[j] + 1).min(row[j + 1] + 1);
diag = row[j + 1];
row[j + 1] = next;
}
}
row[b.len()]
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Progress<'a> {
pub done: u64,
pub total: u64,
pub retrying: Option<&'a Error>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Hub {
root: PathBuf,
from: Option<&'static str>,
}
impl Default for Hub {
fn default() -> Self {
Self::new()
}
}
impl Hub {
pub fn new() -> Self {
Self::from_env(|k| std::env::var_os(k).filter(|v| !v.is_empty()))
}
pub fn at(root: impl Into<PathBuf>) -> Self {
Self { root: root.into(), from: None }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn find(&self, name: &str) -> Option<PathBuf> {
let m = lookup(name).ok()?;
self.find_file(&m.repo(), &m.file())
}
pub fn pull(
&self,
name: &str,
progress: impl FnMut(Progress<'_>) -> ControlFlow<()>,
) -> Result<PathBuf> {
let m = lookup(name)?;
self.pull_file(&m.repo(), &m.file(), progress)
}
pub fn remove(&self, name: &str) -> Result<Option<u64>> {
let m = lookup(name)?;
self.remove_file(&m.repo(), &m.file())
}
}
impl fmt::Display for Hub {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.root.display())?;
self.from.map_or(Ok(()), |v| write!(f, " (set by {v})"))
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
fn name(arg: &str) -> &'static str {
lookup(arg).unwrap().name
}
fn error(arg: &str) -> String {
lookup(arg).unwrap_err().to_string()
}
#[test]
fn short_and_full_names() {
assert_eq!(name("parakeet-v3"), "parakeet-v3");
assert_eq!(name("parakeet-tdt-0.6b-v3"), "parakeet-v3");
assert_eq!(name("gigaam-v3"), "gigaam-v3");
assert_eq!(name("gigaam-v3-e2e-rnnt"), "gigaam-v3");
assert_eq!(name("gigaam-v3-ctc"), "gigaam-v3-ctc");
let m = lookup("gigaam-v3").unwrap();
assert_eq!(m.repo(), "handy-computer/gigaam-v3-e2e-rnnt-gguf");
assert_eq!(m.file(), "gigaam-v3-e2e-rnnt-Q8_0.gguf");
assert_eq!(name("whisper-large-v3-turbo"), "whisper-turbo");
let m = lookup("whisper-turbo").unwrap();
assert_eq!(m.repo(), "handy-computer/whisper-large-v3-turbo-gguf");
assert_eq!(m.file(), "whisper-large-v3-turbo-Q8_0.gguf");
assert_eq!(name("Breeze-ASR-25"), "breeze-asr-25");
let m = lookup("breeze-asr-25").unwrap();
assert_eq!(m.repo(), "handy-computer/Breeze-ASR-25-gguf");
assert_eq!(m.file(), "Breeze-ASR-25-Q8_0.gguf");
}
#[test]
fn names_are_unambiguous() {
for (i, a) in MODELS.iter().enumerate() {
for b in &MODELS[i + 1..] {
for n in [b.name, b.full_name] {
assert!(a.name != n && a.full_name != n, "{} and {} share {n}", a.full_name, b.full_name);
}
}
}
}
#[test]
fn unknown_names() {
assert!(error("parakeet3").contains("did you mean parakeet-v3?"));
assert!(error("gigam-v3").contains("did you mean gigaam-v3?"));
assert!(error("parakeet-tdt-06b-v3").contains("did you mean parakeet-tdt-0.6b-v3?"));
let e = error("whisper");
assert!(e.starts_with("unknown model \"whisper\". Known models: parakeet-v3, "), "{e}");
assert_eq!(distance("kitten", "sitting"), 3);
assert_eq!(distance("", "abc"), 3);
let hub = Hub::at("/nowhere");
assert_eq!(hub.find("whisper"), None);
assert!(matches!(hub.remove("whisper"), Err(Error::UnknownModel { .. })));
}
#[test]
fn model_lists_match() {
let read = |path: &str| fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(path));
let row = |m: &ModelInfo| {
let output = if m.punctuated { "cased, punctuated" } else { "lowercase, no punctuation" };
format!("{:.2} GB | {} | {output} |", m.size as f64 / 1e9, m.languages)
};
let docs: Vec<&str> =
(include_str!("../lib.rs").lines()).filter_map(|l| l.strip_prefix("//! | `")).collect();
let expected: Vec<String> =
MODELS.iter().map(|m| format!("{}` | `{}` | {}", m.name, m.full_name, row(m))).collect();
assert_eq!(docs, expected, "model table in src/lib.rs");
let readme = read("readme.md").unwrap();
let section = readme.split("\n## ").find(|s| s.lines().next().unwrap().ends_with(" Models"));
let readme: Vec<String> = (section.expect("## Models in readme.md").lines())
.filter_map(|l| l.strip_prefix("| `"))
.map(|l| l.replacen(" (default)", "", 1))
.collect();
let expected: Vec<String> =
MODELS.iter().map(|m| format!("{}` | {}", m.name, row(m))).collect();
assert_eq!(readme, expected, "model table in readme.md");
let (makefile, tests) = match (read("Makefile"), read("tests/models.rs")) {
(Ok(makefile), Ok(tests)) => (makefile, tests),
(Err(a), Err(b)) if [&a, &b].iter().all(|e| e.kind() == std::io::ErrorKind::NotFound) => {
return;
}
(makefile, tests) => {
panic!("Makefile: {:?}, tests/models.rs: {:?}", makefile.err(), tests.err())
}
};
let mut full: Vec<&str> = MODELS.iter().map(|m| m.full_name).collect();
full.sort();
let mut make = Vec::new();
for line in makefile.lines().skip_while(|l| !l.starts_with("MODELS := ")) {
make.extend(line.trim_start_matches("MODELS := ").split_whitespace().filter(|w| *w != "\\"));
if !line.ends_with('\\') {
break;
}
}
make.sort();
assert_eq!(make, full, "MODELS in the Makefile");
let mut tested: Vec<&str> = (tests.lines())
.filter_map(|l| l.trim_start().strip_prefix("check(\"")?.split('"').next())
.collect();
tested.sort();
assert_eq!(tested, full, "tests in tests/models.rs");
}
}