use anyhow::{Context, Result};
use colored::Colorize;
use kjarni::ImageIndex;
use std::path::{Path, PathBuf};
use crate::commands::display;
fn model_dir() -> Result<PathBuf> {
let dir = kjarni::registry::cache_dir().join("laion_CLIP-ViT-B-32-laion2B-s34B-b79K");
anyhow::ensure!(
dir.is_dir(),
"clip-vit-base-32 is not downloaded.\n run: kjarni model download clip-vit-base-32"
);
Ok(dir)
}
pub async fn index(inputs: &[String], output: &str, quiet: bool) -> Result<()> {
let dir = model_dir()?;
if !quiet {
println!("{}", "Loading CLIP...".dimmed());
}
let mut index = ImageIndex::load(&dir).context("loading CLIP")?;
let mut total = 0usize;
let mut failures = Vec::new();
for input in inputs {
let path = Path::new(input);
let report = if path.is_dir() {
index.add_directory(path)?
} else {
match index.add_image(path) {
Ok(()) => kjarni::ScanReport {
added: 1,
..Default::default()
},
Err(e) => kjarni::ScanReport {
skipped: 1,
failures: vec![(path.to_path_buf(), format!("{e:#}"))],
..Default::default()
},
}
};
total += report.added;
failures.extend(report.failures);
if !quiet {
println!(" {input}: {} indexed", report.added);
}
}
if !failures.is_empty() {
eprintln!(
"\n{} {} could not be read:",
"warning:".yellow(),
failures.len()
);
for (path, why) in &failures {
eprintln!(" {}: {why}", path.display());
}
}
index
.save(Path::new(output))
.with_context(|| format!("writing {output}"))?;
if !quiet {
println!("\n{} {total} images -> {output}", "Indexed".green().bold());
}
Ok(())
}
pub async fn search(index_path: &str, query: &str, top_k: usize, quiet: bool) -> Result<()> {
let dir = model_dir()?;
let mut index = ImageIndex::load(&dir).context("loading CLIP")?;
let n = index
.load_entries(Path::new(index_path))
.with_context(|| format!("reading {index_path}"))?;
if !quiet {
println!("{}", format!("Searching {n} images...").dimmed());
}
let hits = index.search(query, top_k)?;
if hits.is_empty() {
println!("No images indexed.");
return Ok(());
}
println!("\n {} {}\n", "Results for".dimmed(), query.white().bold());
let best = hits
.iter()
.map(|h| h.score)
.fold(f32::MIN, f32::max)
.max(1e-6);
for (i, hit) in hits.iter().enumerate() {
println!(" {}. {}", i + 1, hit.path.display().to_string().cyan());
println!(
" {} {:.4}",
display::score_bar((hit.score / best).clamp(0.0, 1.0), 20),
hit.score
);
}
println!();
Ok(())
}