use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
pub struct File {
pub name: &'static str,
pub url: &'static str,
pub mb: u32,
}
pub struct Variant {
pub id: &'static str,
pub title: &'static str,
pub steps: usize,
pub main: File,
pub reference: Option<File>,
}
pub const VARIANTS: &[Variant] = &[
Variant {
id: "h3",
title: "MiniMax H3 (official, ComfyUI int8)",
steps: 20,
main: File { name: "minimax_h3_fl2va_pruned_int8_convrot.safetensors", url: "https://huggingface.co/Comfy-Org/MiniMax-H3/resolve/main/diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors", mb: 21_000 },
reference: Some(File { name: "minimax_h3_ref2va_pruned_int8_convrot.safetensors", url: "https://huggingface.co/Comfy-Org/MiniMax-H3/resolve/main/diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors", mb: 21_000 }),
},
Variant {
id: "fasth3",
title: "FastH3 Omni (8-step distilled, text + references)",
steps: 8,
main: File { name: "fasth3_omniref_8step_pruned_int8_convrot.safetensors", url: "https://huggingface.co/FastVideo/FastH3-Omni-Comfy/resolve/main/diffusion_models/fasth3_omniref_8step_pruned_int8_convrot.safetensors", mb: 22_214 },
reference: None,
},
Variant {
id: "singularity",
title: "Singularity v2 (H3 ref2va fine-tune, int8)",
steps: 20,
main: File { name: "Minimax-h3_Singularity_ref2va_v2_pruned_int8_convrot.safetensors", url: "https://huggingface.co/WAS/Minimax-h3_Singularity_v2_pruned/resolve/main/Minimax-h3_Singularity_ref2va_v2_pruned_int8_convrot.safetensors", mb: 20_968 },
reference: None,
},
];
pub fn find(id: &str) -> Result<&'static Variant> {
let id = id.trim().to_ascii_lowercase();
VARIANTS.iter().find(|v| v.id == id).ok_or_else(|| anyhow::anyhow!("unknown model `{id}` (choose one of: {})", VARIANTS.iter().map(|v| v.id).collect::<Vec<_>>().join(", ")))
}
impl Variant {
fn home(&self, models_dir: &Path, file: &File) -> PathBuf {
models_dir.join("diffusion_models").join(self.id).join(file.name)
}
fn present(&self, models_dir: &Path, file: &File) -> Option<PathBuf> {
[self.home(models_dir, file), models_dir.join("diffusion_models").join(file.name)].into_iter().find(|p| p.is_file())
}
fn ensure_file(&self, models_dir: &Path, file: &File) -> Result<PathBuf> {
if let Some(p) = self.present(models_dir, file) {
return Ok(p);
}
let dest = self.home(models_dir, file);
crate::download::ensure_url(&dest, file.url, file.mb)?;
Ok(dest)
}
pub fn ensure_main(&self, models_dir: &Path) -> Result<PathBuf> {
self.ensure_file(models_dir, &self.main)
}
pub fn reference_path(&self, models_dir: &Path) -> PathBuf {
match &self.reference {
Some(f) => self.present(models_dir, f).unwrap_or_else(|| self.home(models_dir, f)),
None => self.present(models_dir, &self.main).unwrap_or_else(|| self.home(models_dir, &self.main)),
}
}
pub fn downloaded(&self, models_dir: &Path) -> bool {
self.present(models_dir, &self.main).is_some()
}
pub fn ensure_all(&self, models_dir: &Path) -> Result<()> {
self.ensure_file(models_dir, &self.main)?;
if let Some(f) = &self.reference {
self.ensure_file(models_dir, f)?;
}
Ok(())
}
pub fn size_gb(&self) -> f64 {
(self.main.mb + self.reference.as_ref().map_or(0, |f| f.mb)) as f64 * 1.048576 / 1000.0
}
}
pub fn ensure_path(path: &Path) -> Result<()> {
if path.is_file() {
return Ok(());
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
for v in VARIANTS {
for f in std::iter::once(&v.main).chain(v.reference.iter()) {
if f.name == name {
return crate::download::ensure_url(path, f.url, f.mb);
}
}
}
bail!("model file {} not found", path.display())
}
pub fn listing(models_dir: &Path) -> Vec<String> {
VARIANTS
.iter()
.map(|v| format!("{:<12} {:<52} {:>5.1} GB {}", v.id, v.title, v.size_gb(), if v.downloaded(models_dir) { "downloaded" } else { "not downloaded" }))
.collect()
}
pub fn choose(models_dir: &Path) -> Result<Option<&'static Variant>> {
use std::io::{BufRead, IsTerminal, Write};
if !std::io::stdin().is_terminal() {
return Ok(None);
}
eprintln!("\nWhich model?\n");
for (i, l) in listing(models_dir).iter().enumerate() {
eprintln!(" {}) {l}", i + 1);
}
loop {
eprint!("\nnumber or name [1]: ");
std::io::stderr().flush()?;
let mut s = String::new();
if std::io::stdin().lock().read_line(&mut s)? == 0 {
return Ok(None);
}
let s = s.trim();
if s.is_empty() {
return Ok(Some(&VARIANTS[0]));
}
if let Some(v) = s.parse::<usize>().ok().and_then(|n| VARIANTS.get(n.wrapping_sub(1))).or_else(|| find(s).ok()) {
return Ok(Some(v));
}
eprintln!("no such model: {s}");
}
}