use hf_hub::api::sync::Api;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Default)]
pub struct DownloadStatus {
pub file: Mutex<String>,
pub file_done: AtomicU64,
pub file_total: AtomicU64,
pub files_done: AtomicU32,
pub files_total: AtomicU32,
}
impl DownloadStatus {
pub fn snapshot(&self) -> (u64, u64, u32, u32, String) {
(
self.file_done.load(Ordering::Relaxed),
self.file_total.load(Ordering::Relaxed),
self.files_done.load(Ordering::Relaxed),
self.files_total.load(Ordering::Relaxed),
self.file.lock().map(|f| f.clone()).unwrap_or_default(),
)
}
}
struct Observed(Arc<DownloadStatus>);
impl hf_hub::api::Progress for Observed {
fn init(&mut self, size: usize, filename: &str) {
if let Ok(mut f) = self.0.file.lock() {
*f = filename.to_string();
}
self.0.file_total.store(size as u64, Ordering::Relaxed);
self.0.file_done.store(0, Ordering::Relaxed);
}
fn update(&mut self, size: usize) {
self.0.file_done.fetch_add(size as u64, Ordering::Relaxed);
}
fn finish(&mut self) {
self.0.files_done.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub struct DownloadedModel {
pub artifact: PathBuf,
pub kind: ArtifactKind,
pub siblings: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
Safetensors,
Gguf,
Onnx,
}
impl DownloadedModel {
pub fn snapshot_dir(&self) -> Option<&std::path::Path> {
self.artifact.parent()
}
}
const SIBLING_CANDIDATES: &[&str] = &[
"config.json",
"tokenizer.json",
"tokenizer.model",
"tokenizer_config.json",
"special_tokens_map.json",
"generation_config.json",
];
pub fn download_model(model_id: &str) -> Result<DownloadedModel, String> {
download_model_observed(model_id, None)
}
pub fn download_model_observed(
model_id: &str,
status: Option<Arc<DownloadStatus>>,
) -> Result<DownloadedModel, String> {
let api = Api::new().map_err(|e| format!("HF API init failed: {e}"))?;
let repo = api.model(model_id.to_string());
let cache = hf_hub::Cache::default().repo(hf_hub::Repo::model(model_id.to_string()));
let info = repo
.info()
.map_err(|e| format!("repo info for {model_id} failed: {e}"))?;
let filenames: Vec<String> = info.siblings.iter().map(|s| s.rfilename.clone()).collect();
let (artifact_name, kind) = pick_artifact(&filenames).ok_or_else(|| {
format!("no recognized artifact in {model_id}; tried safetensors / gguf / onnx")
})?;
let mut planned: Vec<String> = vec![artifact_name.clone()];
if artifact_name.ends_with(".safetensors.index.json") {
planned.extend(
filenames
.iter()
.filter(|f| f.ends_with(".safetensors") && f.contains("of"))
.cloned(),
);
} else if artifact_name.ends_with(".onnx") {
let data_name = format!("{artifact_name}_data");
if filenames.iter().any(|f| f == &data_name) {
planned.push(data_name);
}
}
for name in SIBLING_CANDIDATES {
if filenames.iter().any(|f| f == *name) {
planned.push(name.to_string());
}
}
if let Some(st) = &status {
st.files_total.store(planned.len() as u32, Ordering::Relaxed);
}
let fetch = |name: &str| -> Result<PathBuf, String> {
if let Some(st) = &status {
if let Some(p) = cache.get(name) {
st.files_done.fetch_add(1, Ordering::Relaxed);
return Ok(p);
}
log::info!("Downloading: {name}");
return repo
.download_with_progress(name, Observed(st.clone()))
.map_err(|e| format!("download {name} from {model_id} failed: {e}"));
}
repo.get(name)
.map_err(|e| format!("download {name} from {model_id} failed: {e}"))
};
let artifact = fetch(&artifact_name)?;
let mut siblings: Vec<PathBuf> = Vec::new();
for name in planned.iter().skip(1) {
match fetch(name) {
Ok(p) => siblings.push(p),
Err(e) if name.ends_with(".safetensors") || name.ends_with("_data") => {
return Err(e)
}
Err(e) => log::warn!("sibling {name} listed but download failed: {e}"),
}
}
Ok(DownloadedModel {
artifact,
kind,
siblings,
})
}
fn pick_artifact(filenames: &[String]) -> Option<(String, ArtifactKind)> {
if filenames.iter().any(|f| f == "model.safetensors") {
return Some(("model.safetensors".to_string(), ArtifactKind::Safetensors));
}
if filenames
.iter()
.any(|f| f == "model.safetensors.index.json")
{
return Some((
"model.safetensors.index.json".to_string(),
ArtifactKind::Safetensors,
));
}
if let Some(name) = filenames.iter().find(|f| f.ends_with(".gguf")) {
return Some((name.clone(), ArtifactKind::Gguf));
}
if let Some(name) = filenames.iter().find(|f| f.ends_with(".onnx")) {
return Some((name.clone(), ArtifactKind::Onnx));
}
None
}