use std::path::PathBuf;
mod embedder;
pub use embedder::{LocalEmbedder, LocalModelError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
MacosArm64,
Standard,
}
impl Platform {
#[must_use]
pub fn host() -> Self {
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Self::MacosArm64
} else {
Self::Standard
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::MacosArm64 => "macos-arm64",
Self::Standard => "standard",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ModelFile {
pub name: &'static str,
pub url: &'static str,
pub sha256: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelVariant {
pub platform: Platform,
pub files: &'static [ModelFile],
}
#[derive(Debug, Clone, Copy)]
pub struct ModelSpec {
pub name: &'static str,
pub dim: usize,
pub licence: &'static str,
pub description: &'static str,
pub size_mib: u32,
pub variants: &'static [ModelVariant],
}
impl ModelSpec {
#[must_use]
pub fn variant_for(&self, platform: Platform) -> Option<&ModelVariant> {
self.variants
.iter()
.find(|v| v.platform == platform)
.or_else(|| {
self.variants
.iter()
.find(|v| v.platform == Platform::Standard)
})
}
}
pub const REGISTRY: &[ModelSpec] = &[ModelSpec {
name: "all-minilm-l6-v2",
dim: 384,
licence: "Apache-2.0",
description: "sentence-transformers/all-MiniLM-L6-v2 — small, fast general-purpose embeddings",
size_mib: 90,
variants: &[ModelVariant {
platform: Platform::Standard,
files: &[
ModelFile {
name: "config.json",
url: "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config.json",
sha256: "",
},
ModelFile {
name: "tokenizer.json",
url: "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json",
sha256: "",
},
ModelFile {
name: "model.safetensors",
url: "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/model.safetensors",
sha256: "",
},
],
}],
}];
#[must_use]
pub fn find(name: &str) -> Option<&'static ModelSpec> {
REGISTRY.iter().find(|m| m.name == name)
}
fn store_root_from(roteiro_home: Option<PathBuf>, home: Option<PathBuf>) -> PathBuf {
if let Some(dir) = roteiro_home {
return dir.join("models");
}
home.unwrap_or_else(|| PathBuf::from("."))
.join(".roteiro")
.join("models")
}
#[must_use]
pub fn store_root() -> PathBuf {
store_root_from(
std::env::var_os("ROTEIRO_HOME").map(PathBuf::from),
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from),
)
}
#[must_use]
pub fn model_dir(name: &str) -> PathBuf {
store_root().join(name)
}
#[must_use]
pub fn is_installed(name: &str, variant: &ModelVariant) -> bool {
let dir = model_dir(name);
variant.files.iter().all(|f| dir.join(f.name).exists())
}
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(64);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
#[must_use]
pub fn verify_sha256(bytes: &[u8], expected: &str) -> bool {
expected.is_empty() || sha256_hex(bytes).eq_ignore_ascii_case(expected)
}
pub fn ensure_model_dir(name: &str) -> std::io::Result<PathBuf> {
let dir = model_dir(name);
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
#[cfg(test)]
mod tests {
use super::{Platform, REGISTRY, find, sha256_hex, store_root, verify_sha256};
use std::path::Path;
#[test]
fn registry_entries_are_well_formed() {
assert!(!REGISTRY.is_empty());
for spec in REGISTRY {
assert!(!spec.name.is_empty());
assert!(spec.dim > 0);
assert!(!spec.variants.is_empty());
assert!(
spec.variants
.iter()
.any(|v| v.platform == Platform::Standard),
"{} needs a Standard variant",
spec.name,
);
let v = spec.variant_for(Platform::host()).expect("host variant");
assert!(!v.files.is_empty());
}
}
#[test]
fn variant_selection_falls_back_to_standard() {
let spec = find("all-minilm-l6-v2").expect("registered");
let mac = spec.variant_for(Platform::MacosArm64).expect("mac");
let std = spec.variant_for(Platform::Standard).expect("std");
assert_eq!(mac.platform, Platform::Standard);
assert_eq!(std.platform, Platform::Standard);
}
#[test]
fn platform_host_is_stable() {
let p = Platform::host();
assert!(matches!(p, Platform::MacosArm64 | Platform::Standard));
assert!(!p.as_str().is_empty());
}
#[test]
fn store_root_resolution() {
use super::store_root_from;
use std::path::PathBuf;
assert_eq!(
store_root_from(
Some(PathBuf::from("/opt/rt")),
Some(PathBuf::from("/home/u"))
),
Path::new("/opt/rt/models"),
);
assert_eq!(
store_root_from(None, Some(PathBuf::from("/home/u"))),
Path::new("/home/u/.roteiro/models"),
);
assert!(store_root().ends_with("models"));
}
#[test]
fn sha256_and_verify() {
let want = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
assert_eq!(sha256_hex(b"abc"), want);
assert!(verify_sha256(b"abc", want));
assert!(verify_sha256(b"abc", &want.to_uppercase()));
assert!(!verify_sha256(b"abc", "00"));
assert!(verify_sha256(b"anything", ""));
}
}