mecha10_models/services/model_service/
catalog.rs1use anyhow::{Context, Result};
18use mecha10_core::model::PreprocessingConfig;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use std::time::Duration;
22
23pub async fn fetch_catalog() -> Result<Vec<ModelCatalogEntry>> {
33 let url = mecha10_cli_core::paths::urls::mecha10_api_path("/models/catalog");
34
35 let client = reqwest::Client::builder()
36 .user_agent("mecha10-cli")
37 .timeout(Duration::from_secs(10))
38 .build()
39 .context("Failed to build HTTP client")?;
40
41 let response = client
42 .get(&url)
43 .send()
44 .await
45 .context(format!("Failed to fetch model catalog from {url}"))?
46 .error_for_status()
47 .context(format!("Model catalog request to {url} failed"))?;
48
49 response
50 .json::<Vec<ModelCatalogEntry>>()
51 .await
52 .context(format!("Malformed model catalog response from {url}"))
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
57pub struct ModelCatalogEntry {
58 #[schemars(example = &"yolov8n-face")]
60 pub name: String,
61 #[schemars(example = &"YOLOv8 Nano - Fast face detection")]
63 pub description: String,
64 #[schemars(example = &"object-detection")]
66 pub task: String,
67 #[schemars(example = &"deepghs/yolo-face")]
69 pub repo: String,
70 #[schemars(example = &"yolov8n-face/model.onnx")]
72 pub filename: String,
73
74 #[serde(default)]
76 #[schemars(example = &"yolo")]
77 pub preprocessing_preset: Option<String>,
78
79 #[serde(default)]
81 #[schemars(example = vec!["face".to_string()])]
82 pub classes: Vec<String>,
83
84 #[serde(default)]
86 #[schemars(example = Some(QuantizeConfig { enabled: true, method: "dynamic_int8".to_string() }))]
87 pub quantize: Option<QuantizeConfig>,
88
89 #[serde(default)]
95 #[schemars(example = Some([128_u32, 256_u32]))]
96 pub input_size: Option<[u32; 2]>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
101pub struct QuantizeConfig {
102 #[schemars(example = true)]
104 pub enabled: bool,
105 #[schemars(example = &"dynamic_int8")]
107 pub method: String,
108}
109
110#[derive(Debug, Clone, Copy)]
111pub enum PreprocessingPreset {
112 ImageNet,
113 Yolo,
114 Coco,
115 Zero255,
116}
117
118impl PreprocessingPreset {
119 pub fn from_name(name: &str) -> Result<Self> {
121 match name.to_lowercase().as_str() {
122 "imagenet" => Ok(Self::ImageNet),
123 "yolo" => Ok(Self::Yolo),
124 "coco" => Ok(Self::Coco),
125 "zero255" | "0-255" => Ok(Self::Zero255),
126 _ => anyhow::bail!("Unknown preprocessing preset: {}", name),
127 }
128 }
129
130 pub fn to_config(self) -> PreprocessingConfig {
132 match self {
133 Self::ImageNet | Self::Coco => PreprocessingConfig {
134 mean: [0.485, 0.456, 0.406],
135 std: [0.229, 0.224, 0.225],
136 channel_order: "RGB".to_string(),
137 },
138 Self::Yolo => PreprocessingConfig {
139 mean: [0.0, 0.0, 0.0],
140 std: [255.0, 255.0, 255.0],
141 channel_order: "RGB".to_string(),
142 },
143 Self::Zero255 => PreprocessingConfig {
144 mean: [0.0, 0.0, 0.0],
145 std: [1.0, 1.0, 1.0],
146 channel_order: "RGB".to_string(),
147 },
148 }
149 }
150}
151
152#[cfg(feature = "model-download")]
153#[derive(Debug, Clone, Deserialize)]
154pub(super) struct HFPreprocessorConfig {
155 #[serde(default)]
156 pub(super) image_mean: Option<Vec<f32>>,
157 #[serde(default)]
158 pub(super) image_std: Option<Vec<f32>>,
159 #[serde(default)]
160 pub(super) size: Option<HFSize>,
161 #[serde(default)]
162 pub(super) crop_size: Option<HFSize>,
163}
164
165#[cfg(feature = "model-download")]
166#[derive(Debug, Clone, Deserialize)]
167#[serde(untagged)]
168pub(super) enum HFSize {
169 Dict { height: u32, width: u32 },
170 ShortestEdge { shortest_edge: u32 },
171 Single(u32),
172}
173
174#[cfg(feature = "model-download")]
175impl HFPreprocessorConfig {
176 pub(super) fn to_preprocessing(&self) -> PreprocessingConfig {
177 PreprocessingConfig {
178 mean: [
179 self.image_mean.as_ref().and_then(|v| v.first()).copied().unwrap_or(0.0),
180 self.image_mean.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(0.0),
181 self.image_mean.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(0.0),
182 ],
183 std: [
184 self.image_std.as_ref().and_then(|v| v.first()).copied().unwrap_or(1.0),
185 self.image_std.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(1.0),
186 self.image_std.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(1.0),
187 ],
188 channel_order: "RGB".to_string(),
189 }
190 }
191
192 pub(super) fn input_size(&self) -> Option<[u32; 2]> {
193 if let Some(crop_size) = &self.crop_size {
194 return match crop_size {
195 HFSize::Dict { height, width } => Some([*width, *height]),
196 HFSize::ShortestEdge { shortest_edge } => Some([*shortest_edge, *shortest_edge]),
197 HFSize::Single(s) => Some([*s, *s]),
198 };
199 }
200
201 match &self.size {
202 Some(HFSize::Dict { height, width }) => Some([*width, *height]),
203 Some(HFSize::ShortestEdge { shortest_edge }) => Some([*shortest_edge, *shortest_edge]),
204 Some(HFSize::Single(s)) => Some([*s, *s]),
205 None => None,
206 }
207 }
208}