use std::{
fs,
io::Read,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, ensure};
use sha2::{Digest, Sha256};
const REPOSITORY: &str = "minishlab/potion-base-8M";
const REVISION: &str = "bf8b056651a2c21b8d2565580b8569da283cab23";
const FILES: [(&str, &str); 3] = [
("config.json", "2a6ac0e9aaa356a68a5688070db78fc3a464fefe85d2f06a1905ce3718687553"),
("tokenizer.json", "e67e803f624fb4d67dea1c730d06e1067e1b14d830e2c2202569e3ef0f70bb50"),
("model.safetensors", "f65d0f325faadc1e121c319e2faa41170d3fa07d8c89abd48ca5358d9a223de2"),
];
pub struct ModelFiles {
pub config: Vec<u8>,
pub tokenizer: Vec<u8>,
pub model: Vec<u8>,
}
pub fn ensure() -> Result<ModelFiles> {
let directory = cache_directory()?;
fs::create_dir_all(&directory)
.with_context(|| format!("creating model cache at {}", directory.display()))?;
let mut contents = FILES.map(|(name, checksum)| {
let path = directory.join(name);
match fs::read(&path) {
Ok(bytes) if digest(&bytes) == checksum => Some(bytes),
_ => None,
}
});
if contents.iter().any(Option::is_none) {
eprintln!(
"findnerd: downloading {REPOSITORY} model to {} (one-time, ~29 MB)",
directory.display()
);
for (slot, (name, checksum)) in contents.iter_mut().zip(FILES) {
if slot.is_none() {
*slot = Some(download(&directory, name, checksum)?);
}
}
}
let [config, tokenizer, model] = contents.map(|bytes| bytes.expect("all slots filled above"));
Ok(ModelFiles { config, tokenizer, model })
}
fn cache_directory() -> Result<PathBuf> {
let base = dirs::cache_dir().context(
"no user cache directory available; install with `cargo install findnerd \
--no-default-features --features embed-model` from a git checkout for a self-contained \
binary",
)?;
Ok(base.join("findnerd").join("model").join(&REVISION[..8]))
}
fn download(directory: &Path, name: &str, checksum: &str) -> Result<Vec<u8>> {
let url = format!("https://huggingface.co/{REPOSITORY}/resolve/{REVISION}/{name}");
let mut response = ureq::get(&url)
.call()
.with_context(|| format!("downloading {url}"))?;
let mut bytes = Vec::new();
response
.body_mut()
.as_reader()
.read_to_end(&mut bytes)
.with_context(|| format!("reading {url}"))?;
ensure!(
digest(&bytes) == checksum,
"checksum mismatch for {name} from {url}; refusing to cache"
);
let temporary = directory.join(format!(".{name}.partial"));
fs::write(&temporary, &bytes)
.and_then(|()| fs::rename(&temporary, directory.join(name)))
.with_context(|| format!("caching {name} in {}", directory.display()))?;
Ok(bytes)
}
fn digest(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}