use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
const LATEON_CODE_EDGE_BASE_URL: &str =
"https://huggingface.co/lightonai/LateOn-Code-edge/resolve/main";
const LATEON_CODE_EDGE_FILES: &[&str] = &["model_int8.onnx", "tokenizer.json", "onnx_config.json"];
const LATEON_CODE_EDGE_DIR: &str = "LateOn-Code-edge";
pub const STATIC_TOKEN_TABLE_FILE: &str = "static_token_table.bin";
pub const FROZEN_CENTROIDS_FILE: &str = "frozen_centroids.npy";
pub const CINDER_MIXER_FILE: &str = "cinder_mixer.bin";
pub const CINDER_SHORTLISTS_FILE: &str = "cinder_shortlists.bin";
const EMBER_CINDER_ARTIFACT_BASE_URL: &str =
"https://github.com/MisterTK/semantex/releases/download";
const EMBER_CINDER_ARTIFACT_RELEASE_TAG: &str = "v1.1.0";
const EMBER_CINDER_ARTIFACT_FILES: &[&str] = &[
STATIC_TOKEN_TABLE_FILE,
FROZEN_CENTROIDS_FILE,
CINDER_MIXER_FILE,
CINDER_SHORTLISTS_FILE,
];
fn ember_cinder_artifact_url(file_name: &str) -> String {
format!("{EMBER_CINDER_ARTIFACT_BASE_URL}/{EMBER_CINDER_ARTIFACT_RELEASE_TAG}/{file_name}")
}
pub fn static_token_table_path(model_dir: &Path) -> PathBuf {
model_dir.join(STATIC_TOKEN_TABLE_FILE)
}
pub fn frozen_centroids_path(model_dir: &Path) -> PathBuf {
model_dir.join(FROZEN_CENTROIDS_FILE)
}
pub fn cinder_mixer_path(model_dir: &Path) -> PathBuf {
model_dir.join(CINDER_MIXER_FILE)
}
pub fn cinder_shortlists_path(model_dir: &Path) -> PathBuf {
model_dir.join(CINDER_SHORTLISTS_FILE)
}
#[must_use]
pub fn colbert_model_dir(models_dir: &Path) -> PathBuf {
models_dir.join(LATEON_CODE_EDGE_DIR)
}
#[allow(clippy::similar_names)]
pub fn ensure_colbert_model(models_dir: &Path) -> Result<PathBuf> {
let model_dir = models_dir.join(LATEON_CODE_EDGE_DIR);
if model_dir.join("model_int8.onnx").exists() {
return Ok(model_dir);
}
fs::create_dir_all(&model_dir)
.with_context(|| format!("Failed to create model dir: {}", model_dir.display()))?;
tracing::info!("Downloading LateOn-Code-edge ColBERT model (~17MB)...");
for file_name in LATEON_CODE_EDGE_FILES {
let url = format!("{LATEON_CODE_EDGE_BASE_URL}/{file_name}");
let dest = model_dir.join(file_name);
if !dest.exists() {
download_file(&url, &dest)
.with_context(|| format!("Failed to download {file_name} for LateOn-Code-edge"))?;
}
}
Ok(model_dir)
}
#[allow(clippy::similar_names)]
pub fn is_colbert_downloaded(models_dir: &Path) -> bool {
let model_dir = models_dir.join(LATEON_CODE_EDGE_DIR);
LATEON_CODE_EDGE_FILES
.iter()
.all(|f| model_dir.join(f).exists())
}
pub fn ensure_ember_cinder_artifacts(model_dir: &Path) -> Result<()> {
fs::create_dir_all(model_dir)
.with_context(|| format!("Failed to create model dir: {}", model_dir.display()))?;
for file_name in EMBER_CINDER_ARTIFACT_FILES {
let dest = model_dir.join(file_name);
if dest.exists() {
continue;
}
tracing::info!(
"Downloading Ember/Cinder artifact {file_name} from release \
{EMBER_CINDER_ARTIFACT_RELEASE_TAG}..."
);
let url = ember_cinder_artifact_url(file_name);
download_file(&url, &dest).with_context(|| {
format!(
"Failed to download {file_name} from release {EMBER_CINDER_ARTIFACT_RELEASE_TAG}"
)
})?;
}
Ok(())
}
#[must_use]
pub fn are_ember_cinder_artifacts_present(model_dir: &Path) -> bool {
EMBER_CINDER_ARTIFACT_FILES
.iter()
.all(|f| model_dir.join(f).exists())
}
pub(crate) fn download_file(url: &str, dest: &Path) -> Result<()> {
let resp = ureq::get(url)
.call()
.with_context(|| format!("HTTP GET failed for {url}"))?;
let total_size: u64 = resp
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.expect("valid progress template")
.progress_chars("#>-"),
);
let file_name = dest
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
pb.set_message(file_name);
let parent = dest.parent().context("dest has no parent directory")?;
let tmp_path = parent.join(format!(
".tmp_{}",
dest.file_name()
.map_or_else(|| "download".into(), |n| n.to_string_lossy().to_string())
));
let mut tmp_file = fs::File::create(&tmp_path)
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
let mut reader = resp.into_body().into_reader();
let mut buf = [0u8; 8192];
loop {
let n = reader
.read(&mut buf)
.context("Failed to read response body")?;
if n == 0 {
break;
}
tmp_file.write_all(&buf[..n])?;
pb.inc(n as u64);
}
tmp_file.flush()?;
drop(tmp_file);
fs::rename(&tmp_path, dest).with_context(|| {
format!(
"Failed to rename {} -> {}",
tmp_path.display(),
dest.display()
)
})?;
pb.finish_with_message("done");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ember_cinder_artifact_url_is_pinned_to_release_tag() {
assert_eq!(
ember_cinder_artifact_url("cinder_mixer.bin"),
"https://github.com/MisterTK/semantex/releases/download/v1.1.0/cinder_mixer.bin"
);
for f in EMBER_CINDER_ARTIFACT_FILES {
assert!(
ember_cinder_artifact_url(f)
.starts_with("https://github.com/MisterTK/semantex/releases/download/v1.1.0/"),
"{f} must resolve under the pinned release tag"
);
}
}
#[test]
fn ensure_ember_cinder_artifacts_skips_when_all_present() {
let tmp = tempfile::TempDir::new().unwrap();
for f in EMBER_CINDER_ARTIFACT_FILES {
std::fs::write(tmp.path().join(f), b"placeholder").unwrap();
}
assert!(are_ember_cinder_artifacts_present(tmp.path()));
ensure_ember_cinder_artifacts(tmp.path())
.expect("all artifacts present → no download attempted → Ok");
}
#[test]
fn are_ember_cinder_artifacts_present_requires_all_four() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(
!are_ember_cinder_artifacts_present(tmp.path()),
"none present"
);
for (i, f) in EMBER_CINDER_ARTIFACT_FILES.iter().enumerate() {
std::fs::write(tmp.path().join(f), b"x").unwrap();
let expect_all = i + 1 == EMBER_CINDER_ARTIFACT_FILES.len();
assert_eq!(
are_ember_cinder_artifacts_present(tmp.path()),
expect_all,
"presence must require ALL four (after {} of {})",
i + 1,
EMBER_CINDER_ARTIFACT_FILES.len()
);
}
}
}