use crate::model_package::{ModelPackage, ModelTask};
use crate::{ResolvedRuntimeVariant, RuntimeRegistry, RuntimeResolver};
use latexsnipper_foundation::{Result, SnipperError};
use latexsnipper_model::{RuntimeVariant, VariantStatus};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelManifest {
pub id: String,
pub task: ModelTask,
pub version: String,
pub adapter: String,
pub input: ManifestTensorSpec,
pub output: Vec<ManifestTensorSpec>,
#[serde(default)]
pub files: ModelFiles,
#[serde(default)]
pub preprocessing: Option<ManifestPreprocessing>,
#[serde(default)]
pub decoding: Option<ManifestDecoding>,
#[serde(default)]
pub checksums: HashMap<String, String>,
#[serde(default, rename = "runtimeVariants")]
pub runtime_variants: Vec<RuntimeVariant>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestTensorSpec {
pub name: String,
pub shape: Vec<i64>,
pub dtype: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ModelFiles {
#[serde(default)]
pub primary: Option<String>,
#[serde(default)]
pub encoder: Option<String>,
#[serde(default)]
pub decoder: Option<String>,
#[serde(default)]
pub tokenizer: Option<String>,
#[serde(default)]
pub config: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestPreprocessing {
#[serde(default)]
pub resize: Option<ManifestResize>,
#[serde(default)]
pub mean: Option<Vec<f32>>,
#[serde(default)]
pub std: Option<Vec<f32>>,
#[serde(default)]
pub color_format: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestResize {
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
#[serde(default)]
pub keep_ratio: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestDecoding {
#[serde(rename = "type")]
pub decoding_type: String,
#[serde(default)]
pub beam_width: Option<usize>,
#[serde(default)]
pub blank_id: Option<usize>,
#[serde(default)]
pub output_layout: Option<String>,
#[serde(default)]
pub logits_kind: Option<String>,
#[serde(default)]
pub temperature: Option<f32>,
}
use serde::{Deserialize, Serialize};
pub type AdapterFactory =
Box<dyn Fn(&ModelManifest, &Path) -> Result<Box<dyn ModelPackage>> + Send + Sync>;
pub struct ModelRegistry {
models: HashMap<String, ModelEntry>,
dirs: Vec<PathBuf>,
adapter_factories: HashMap<String, AdapterFactory>,
}
struct ModelEntry {
manifest: ModelManifest,
dir: PathBuf,
}
impl ModelRegistry {
pub fn new() -> Self {
Self {
models: HashMap::new(),
dirs: Vec::new(),
adapter_factories: HashMap::new(),
}
}
pub fn register_adapter(
&mut self,
adapter_name: impl Into<String>,
factory: impl Fn(&ModelManifest, &Path) -> Result<Box<dyn ModelPackage>> + Send + Sync + 'static,
) {
self.adapter_factories
.insert(adapter_name.into(), Box::new(factory));
}
pub fn create_package(
&self,
manifest: &ModelManifest,
model_dir: &Path,
) -> Result<Option<Box<dyn ModelPackage>>> {
let factory = match self.adapter_factories.get(&manifest.adapter) {
Some(f) => f,
None => return Ok(None),
};
let package = factory(manifest, model_dir)?;
Ok(Some(package))
}
pub fn registered_adapters(&self) -> Vec<&str> {
self.adapter_factories.keys().map(|s| s.as_str()).collect()
}
pub fn from_dir(path: impl AsRef<Path>) -> Result<Self> {
let mut registry = Self::new();
registry.register_dir(path)?;
Ok(registry)
}
pub fn register_dir(&mut self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref().to_path_buf();
if !path.is_dir() {
return Err(SnipperError::Model(format!(
"Not a directory: {}",
path.display()
)));
}
self.dirs.push(path.clone());
for entry in std::fs::read_dir(&path)
.map_err(|e| SnipperError::Model(format!("Failed to read {}: {}", path.display(), e)))?
{
let entry =
entry.map_err(|e| SnipperError::Model(format!("Failed to read entry: {}", e)))?;
if !entry.path().is_dir() {
continue;
}
let model_dir = entry.path();
let manifest_path = model_dir.join("manifest.toml");
if manifest_path.exists() {
if let Ok(manifest) = Self::load_manifest(&manifest_path) {
let id = manifest.id.clone();
self.models.insert(
id,
ModelEntry {
manifest,
dir: model_dir,
},
);
}
}
}
Ok(())
}
fn load_manifest(path: &Path) -> Result<ModelManifest> {
let content = std::fs::read_to_string(path).map_err(|e| {
SnipperError::Model(format!("Failed to read {}: {}", path.display(), e))
})?;
toml::from_str(&content)
.map_err(|e| SnipperError::Model(format!("Failed to parse {}: {}", path.display(), e)))
}
pub fn get(&self, id: &str) -> Option<&ModelManifest> {
self.models.get(id).map(|e| &e.manifest)
}
pub fn get_dir(&self, id: &str) -> Option<&Path> {
self.models.get(id).map(|e| e.dir.as_path())
}
pub fn find_by_task(&self, task: ModelTask) -> Vec<(&ModelManifest, &Path)> {
self.models
.iter()
.filter(|(_, e)| e.manifest.task == task)
.map(|(_, e)| (&e.manifest, e.dir.as_path()))
.collect()
}
pub fn list_ids(&self) -> Vec<&str> {
self.models.keys().map(|s| s.as_str()).collect()
}
pub fn has(&self, id: &str) -> bool {
self.models.contains_key(id)
}
}
impl ModelManifest {
pub fn resolve_runtime(
&self,
registry: &RuntimeRegistry,
model_dir: &Path,
) -> Result<ResolvedRuntimeVariant> {
self.resolve_runtime_variant(registry, model_dir, None)
}
pub fn resolve_runtime_variant(
&self,
registry: &RuntimeRegistry,
model_dir: &Path,
preferred_variant: Option<&str>,
) -> Result<ResolvedRuntimeVariant> {
let variants = self.all_variants(model_dir);
RuntimeResolver::new(registry).resolve(&self.id, &variants, model_dir, preferred_variant)
}
fn implicit_onnx_variant(&self, _model_dir: &Path) -> Option<RuntimeVariant> {
let mut artifacts = BTreeMap::new();
if let Some(ref p) = self.files.primary {
artifacts.insert("model".to_string(), p.clone());
}
if let Some(ref e) = self.files.encoder {
artifacts.insert("encoder".to_string(), e.clone());
}
if let Some(ref d) = self.files.decoder {
artifacts.insert("decoder".to_string(), d.clone());
}
if let Some(ref t) = self.files.tokenizer {
artifacts.insert("tokenizer".to_string(), t.clone());
}
if let Some(ref c) = self.files.config {
artifacts.insert("config".to_string(), c.clone());
}
if artifacts.is_empty() {
return None;
}
Some(RuntimeVariant {
id: "onnx-default".to_string(),
runtime: "onnx-runtime".to_string(),
status: VariantStatus::Stable,
priority: 0,
artifacts,
options: None,
platforms: Vec::new(),
capabilities: Vec::new(),
fallbacks: Vec::new(),
})
}
pub fn all_variants(&self, model_dir: &Path) -> Vec<RuntimeVariant> {
if !self.runtime_variants.is_empty() {
self.runtime_variants.clone()
} else if let Some(v) = self.implicit_onnx_variant(model_dir) {
vec![v]
} else {
vec![]
}
}
}
impl Default for ModelRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn register_builtin_adapters(_registry: &mut ModelRegistry) {
}