use std::collections::{BTreeMap, BTreeSet};
use crate::discovery::gguf_shards;
use crate::install::bytes::saturating_sum;
const WEIGHT_EXTENSIONS: [&str; 6] = ["safetensors", "gguf", "bin", "ckpt", "pt", "pth"];
const EXCLUDED_EXTENSIONS: [&str; 12] = [
"md", "png", "jpg", "jpeg", "gif", "webp", "svg", "msgpack", "h5", "ot", "onnx", "tflite",
];
const EXCLUDED_DIRECTORIES: [&str; 3] = ["onnx", "openvino", "coreml"];
const QUANT_PREFERENCE: [&str; 6] = ["q4_k_m", "q4_0", "q5_k_m", "q6_k", "q8_0", "f16"];
const COMPANION_CAP: i64 = 10 << 20;
const CONFIG_CAP: i64 = 100 << 20;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HFSibling {
pub rfilename: String,
pub bytes: Option<i64>,
pub sha256: Option<String>,
}
impl HFSibling {
pub fn new(rfilename: impl Into<String>, bytes: Option<i64>) -> Self {
Self {
rfilename: rfilename.into(),
bytes,
sha256: None,
}
}
pub fn with_sha256(mut self, sha256: Option<String>) -> Self {
self.sha256 = sha256;
self
}
pub fn is_weight(&self) -> bool {
is_weight_path(&self.rfilename)
}
}
pub fn is_weight_path(path: &str) -> bool {
WEIGHT_EXTENSIONS.contains(&file_extension(path).as_str())
}
pub fn file_extension(path: &str) -> String {
match path.rfind('.') {
Some(dot) if dot > 0 => path[dot + 1..].to_lowercase(),
_ => String::new(),
}
}
pub fn select(siblings: &[HFSibling]) -> Vec<HFSibling> {
let kept: Vec<HFSibling> = siblings
.iter()
.filter(|s| is_eligible(s))
.cloned()
.collect();
let ggufs: Vec<HFSibling> = kept
.iter()
.filter(|s| s.rfilename.to_lowercase().ends_with(".gguf"))
.cloned()
.collect();
if !ggufs.is_empty() {
let others: Vec<HFSibling> = kept
.iter()
.filter(|s| !ggufs.contains(s))
.cloned()
.collect();
return gguf_selection(&ggufs, &others);
}
if kept.iter().any(|s| s.rfilename == "model_index.json") {
return diffusers_selection(&kept);
}
transformers_selection(&kept)
}
fn segments(path: &str) -> Vec<&str> {
path.split('/').filter(|part| !part.is_empty()).collect()
}
fn is_eligible(sibling: &HFSibling) -> bool {
let path = &sibling.rfilename;
let segments = segments(path);
let Some(filename) = segments.last().copied() else {
return false;
};
if segments.iter().any(|segment| segment.starts_with('.')) {
return false;
}
if filename.to_lowercase().starts_with("readme") {
return false;
}
if EXCLUDED_EXTENSIONS.contains(&file_extension(filename).as_str()) {
return false;
}
let stem = filename.to_lowercase();
if stem.starts_with("flax_model") || stem.starts_with("tf_model") {
return false;
}
if segments.len() > 1
&& let Some(first) = segments.first()
&& EXCLUDED_DIRECTORIES.contains(&first.to_lowercase().as_str())
{
return false;
}
true
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct GroupKey {
directory: String,
base: String,
total: usize,
}
fn gguf_selection(ggufs: &[HFSibling], others: &[HFSibling]) -> Vec<HFSibling> {
let mut groups: BTreeMap<GroupKey, Vec<HFSibling>> = BTreeMap::new();
let mut seen_indices: BTreeMap<GroupKey, BTreeSet<usize>> = BTreeMap::new();
for sibling in ggufs {
let segments = segments(&sibling.rfilename);
let filename = segments.last().copied().unwrap_or("");
let directory = segments[..segments.len().saturating_sub(1)].join("/");
if let Some(shard) = gguf_shards::parse(filename) {
let key = GroupKey {
directory,
base: shard.base,
total: shard.total,
};
groups.entry(key.clone()).or_default().push(sibling.clone());
seen_indices.entry(key).or_default().insert(shard.index);
} else {
let stem = &filename[..filename.len() - ".gguf".len()];
let key = GroupKey {
directory,
base: stem.to_owned(),
total: 0,
};
groups.entry(key).or_default().push(sibling.clone());
}
}
let mmproj: Vec<HFSibling> = ggufs
.iter()
.filter(|s| s.rfilename.to_lowercase().contains("mmproj"))
.cloned()
.collect();
let ordered: Vec<Vec<HFSibling>> = groups
.iter()
.filter(|(key, _)| {
!key.base.to_lowercase().contains("mmproj")
&& (key.total == 0 || seen_indices.get(*key).map(BTreeSet::len) == Some(key.total))
})
.map(|(_, group)| group.clone())
.collect();
let chosen = pick_quant_group(&ordered);
if chosen.is_empty() {
return Vec::new();
}
let companions: Vec<HFSibling> = others
.iter()
.filter(|s| s.bytes.unwrap_or(0) <= COMPANION_CAP)
.cloned()
.collect();
let mut result = chosen.clone();
result.extend(mmproj.into_iter().filter(|s| !chosen.contains(s)));
result.extend(companions);
result
}
fn pick_quant_group(groups: &[Vec<HFSibling>]) -> Vec<HFSibling> {
if groups.is_empty() {
return Vec::new();
}
for token in QUANT_PREFERENCE {
if let Some(matched) = groups
.iter()
.find(|group| group.iter().any(|s| matches_quant(&s.rfilename, token)))
{
return matched.clone();
}
}
groups
.iter()
.min_by_key(|group| saturating_sum(group.iter().filter_map(|s| s.bytes)))
.cloned()
.unwrap_or_default()
}
fn matches_quant(rfilename: &str, token: &str) -> bool {
let name = rfilename.to_lowercase();
let mut start = 0;
while let Some(offset) = name[start..].find(token) {
let at = start + offset;
let end = at + token.len();
let before_ok = match name[..at].chars().next_back() {
None => true,
Some(character) => !is_quant_character(character),
};
let after_ok = match name[end..].chars().next() {
None => true,
Some(character) => !is_quant_character(character),
};
if before_ok && after_ok {
return true;
}
start = end;
}
false
}
fn is_quant_character(character: char) -> bool {
character.is_alphanumeric() || character == '_'
}
fn diffusers_selection(kept: &[HFSibling]) -> Vec<HFSibling> {
let paths: BTreeSet<&str> = kept.iter().map(|s| s.rfilename.as_str()).collect();
let tree_has_weights = kept
.iter()
.any(|s| s.rfilename.contains('/') && s.is_weight());
kept.iter()
.filter(|sibling| {
let path = &sibling.rfilename;
if tree_has_weights && !path.contains('/') && sibling.is_weight() {
return false;
}
let ext = file_extension(path);
if ["bin", "ckpt", "pt", "pth"].contains(&ext.as_str())
&& let Some(stem) = path.get(..path.len().saturating_sub(ext.len() + 1))
&& paths.contains(format!("{stem}.safetensors").as_str())
{
return false;
}
for variant in [".fp16.", ".non_ema."] {
if path.contains(variant) && paths.contains(path.replace(variant, ".").as_str()) {
return false;
}
}
true
})
.cloned()
.collect()
}
fn transformers_selection(kept: &[HFSibling]) -> Vec<HFSibling> {
let root: Vec<&HFSibling> = kept.iter().filter(|s| !s.rfilename.contains('/')).collect();
let safetensors: Vec<&HFSibling> = root
.iter()
.copied()
.filter(|s| {
file_extension(&s.rfilename) == "safetensors"
|| s.rfilename.ends_with(".safetensors.index.json")
})
.collect();
let has_safetensors_weight = safetensors
.iter()
.any(|s| file_extension(&s.rfilename) == "safetensors");
let weights: Vec<HFSibling> = if has_safetensors_weight {
safetensors.iter().copied().cloned().collect()
} else {
root.iter()
.copied()
.filter(|s| {
s.rfilename.starts_with("pytorch_model")
&& (file_extension(&s.rfilename) == "bin"
|| s.rfilename.ends_with(".bin.index.json"))
})
.cloned()
.collect()
};
let alternatives: BTreeSet<&str> = kept
.iter()
.filter(|s| s.is_weight())
.filter_map(|s| subtree(&s.rfilename))
.collect();
let support: Vec<HFSibling> = kept
.iter()
.filter(|s| {
!s.is_weight()
&& !s.rfilename.ends_with(".index.json")
&& s.bytes.unwrap_or(0) <= CONFIG_CAP
&& subtree(&s.rfilename).is_none_or(|dir| !alternatives.contains(dir))
})
.cloned()
.collect();
let mut result = weights;
result.extend(support);
result
}
fn subtree(path: &str) -> Option<&str> {
let segments = segments(path);
(segments.len() > 1).then(|| segments[0])
}