1use anyhow::Result;
20use mecha10_core::model::PreprocessingConfig;
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23
24fn entry(
25 name: &str,
26 description: &str,
27 task: &str,
28 repo: &str,
29 filename: &str,
30 preprocessing_preset: Option<&str>,
31 classes: &[&str],
32) -> ModelCatalogEntry {
33 ModelCatalogEntry {
34 name: name.to_string(),
35 description: description.to_string(),
36 task: task.to_string(),
37 repo: repo.to_string(),
38 filename: filename.to_string(),
39 preprocessing_preset: preprocessing_preset.map(|p| p.to_string()),
40 classes: classes.iter().map(|c| c.to_string()).collect(),
41 quantize: None,
42 input_size: None,
43 }
44}
45
46pub fn catalog_entries() -> Vec<ModelCatalogEntry> {
50 vec![
51 entry(
52 "yolov8n-face",
53 "YOLOv8 Nano - Fast face detection",
54 "object-detection",
55 "deepghs/yolo-face",
56 "yolov8n-face/model.onnx",
57 Some("yolo"),
58 &["face"],
59 ),
60 entry(
61 "yolov8m-face",
62 "YOLOv8 Medium - Better accuracy face detection",
63 "object-detection",
64 "deepghs/yolo-face",
65 "yolov8m-face/model.onnx",
66 Some("yolo"),
67 &["face"],
68 ),
69 entry(
70 "yolov8n",
71 "YOLOv8 Nano - Fast object detection (80 COCO classes)",
72 "object-detection",
73 "deepghs/yolos",
74 "yolov8n/model.onnx",
75 Some("yolo"),
76 &[],
77 ),
78 entry(
79 "yolov8s",
80 "YOLOv8 Small - Balanced speed/accuracy (80 COCO classes)",
81 "object-detection",
82 "deepghs/yolos",
83 "yolov8s/model.onnx",
84 Some("yolo"),
85 &[],
86 ),
87 entry(
88 "yolov8m",
89 "YOLOv8 Medium - Better accuracy (80 COCO classes)",
90 "object-detection",
91 "deepghs/yolos",
92 "yolov8m/model.onnx",
93 Some("yolo"),
94 &[],
95 ),
96 entry(
97 "yolov8l",
98 "YOLOv8 Large - High accuracy (80 COCO classes)",
99 "object-detection",
100 "deepghs/yolos",
101 "yolov8l/model.onnx",
102 Some("yolo"),
103 &[],
104 ),
105 entry(
106 "yolov8x",
107 "YOLOv8 Extra-Large - Best accuracy (80 COCO classes)",
108 "object-detection",
109 "deepghs/yolos",
110 "yolov8x/model.onnx",
111 Some("yolo"),
112 &[],
113 ),
114 entry(
115 "mobilenet-v2",
116 "MobileNetV2 - Fast image classification (1000 ImageNet classes)",
117 "image-classification",
118 "onnx-community/mobilenet_v2_0.75_160",
119 "onnx/model.onnx",
120 Some("imagenet"),
121 &[],
122 ),
123 entry(
124 "resnet18",
125 "ResNet-18 - Image classification (1000 ImageNet classes)",
126 "image-classification",
127 "onnx-community/resnet-18",
128 "onnx/model.onnx",
129 Some("imagenet"),
130 &[],
131 ),
132 ModelCatalogEntry {
133 input_size: Some([128, 256]),
140 ..entry(
141 "osnet_x0_25",
142 "OSNet x0.25 - Lightweight person re-identification features for DeepSORT (512-dim, MSMT17)",
143 "feature-extraction",
144 "anriha/osnet_x0_25_msmt17",
145 "osnet_x0_25_msmt17.onnx",
146 Some("imagenet"),
147 &[],
148 )
149 },
150 ]
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
155pub struct ModelCatalogEntry {
156 #[schemars(example = &"yolov8n-face")]
158 pub name: String,
159 #[schemars(example = &"YOLOv8 Nano - Fast face detection")]
161 pub description: String,
162 #[schemars(example = &"object-detection")]
164 pub task: String,
165 #[schemars(example = &"deepghs/yolo-face")]
167 pub repo: String,
168 #[schemars(example = &"yolov8n-face/model.onnx")]
170 pub filename: String,
171
172 #[serde(default)]
174 #[schemars(example = &"yolo")]
175 pub preprocessing_preset: Option<String>,
176
177 #[serde(default)]
179 #[schemars(example = vec!["face".to_string()])]
180 pub classes: Vec<String>,
181
182 #[serde(default)]
184 #[schemars(example = Some(QuantizeConfig { enabled: true, method: "dynamic_int8".to_string() }))]
185 pub quantize: Option<QuantizeConfig>,
186
187 #[serde(default)]
193 #[schemars(example = Some([128_u32, 256_u32]))]
194 pub input_size: Option<[u32; 2]>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
199pub struct QuantizeConfig {
200 #[schemars(example = true)]
202 pub enabled: bool,
203 #[schemars(example = &"dynamic_int8")]
205 pub method: String,
206}
207
208#[derive(Debug, Clone, Copy)]
209pub enum PreprocessingPreset {
210 ImageNet,
211 Yolo,
212 Coco,
213 Zero255,
214}
215
216impl PreprocessingPreset {
217 pub fn from_name(name: &str) -> Result<Self> {
219 match name.to_lowercase().as_str() {
220 "imagenet" => Ok(Self::ImageNet),
221 "yolo" => Ok(Self::Yolo),
222 "coco" => Ok(Self::Coco),
223 "zero255" | "0-255" => Ok(Self::Zero255),
224 _ => anyhow::bail!("Unknown preprocessing preset: {}", name),
225 }
226 }
227
228 pub fn to_config(self) -> PreprocessingConfig {
230 match self {
231 Self::ImageNet | Self::Coco => PreprocessingConfig {
232 mean: [0.485, 0.456, 0.406],
233 std: [0.229, 0.224, 0.225],
234 channel_order: "RGB".to_string(),
235 },
236 Self::Yolo => PreprocessingConfig {
237 mean: [0.0, 0.0, 0.0],
238 std: [255.0, 255.0, 255.0],
239 channel_order: "RGB".to_string(),
240 },
241 Self::Zero255 => PreprocessingConfig {
242 mean: [0.0, 0.0, 0.0],
243 std: [1.0, 1.0, 1.0],
244 channel_order: "RGB".to_string(),
245 },
246 }
247 }
248}
249
250#[cfg(feature = "model-download")]
251#[derive(Debug, Clone, Deserialize)]
252pub(super) struct HFPreprocessorConfig {
253 #[serde(default)]
254 pub(super) image_mean: Option<Vec<f32>>,
255 #[serde(default)]
256 pub(super) image_std: Option<Vec<f32>>,
257 #[serde(default)]
258 pub(super) size: Option<HFSize>,
259 #[serde(default)]
260 pub(super) crop_size: Option<HFSize>,
261}
262
263#[cfg(feature = "model-download")]
264#[derive(Debug, Clone, Deserialize)]
265#[serde(untagged)]
266pub(super) enum HFSize {
267 Dict { height: u32, width: u32 },
268 ShortestEdge { shortest_edge: u32 },
269 Single(u32),
270}
271
272#[cfg(feature = "model-download")]
273impl HFPreprocessorConfig {
274 pub(super) fn to_preprocessing(&self) -> PreprocessingConfig {
275 PreprocessingConfig {
276 mean: [
277 self.image_mean.as_ref().and_then(|v| v.first()).copied().unwrap_or(0.0),
278 self.image_mean.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(0.0),
279 self.image_mean.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(0.0),
280 ],
281 std: [
282 self.image_std.as_ref().and_then(|v| v.first()).copied().unwrap_or(1.0),
283 self.image_std.as_ref().and_then(|v| v.get(1)).copied().unwrap_or(1.0),
284 self.image_std.as_ref().and_then(|v| v.get(2)).copied().unwrap_or(1.0),
285 ],
286 channel_order: "RGB".to_string(),
287 }
288 }
289
290 pub(super) fn input_size(&self) -> Option<[u32; 2]> {
291 if let Some(crop_size) = &self.crop_size {
292 return match crop_size {
293 HFSize::Dict { height, width } => Some([*width, *height]),
294 HFSize::ShortestEdge { shortest_edge } => Some([*shortest_edge, *shortest_edge]),
295 HFSize::Single(s) => Some([*s, *s]),
296 };
297 }
298
299 match &self.size {
300 Some(HFSize::Dict { height, width }) => Some([*width, *height]),
301 Some(HFSize::ShortestEdge { shortest_edge }) => Some([*shortest_edge, *shortest_edge]),
302 Some(HFSize::Single(s)) => Some([*s, *s]),
303 None => None,
304 }
305 }
306}
307
308#[cfg(test)]
309mod catalog_entries_tests {
310 use super::*;
311
312 #[test]
313 fn catalog_entries_are_non_empty_and_uniquely_named() {
314 let entries = catalog_entries();
315 assert!(!entries.is_empty());
316
317 let mut names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
318 names.sort();
319 names.dedup();
320 assert_eq!(names.len(), entries.len(), "catalog entry names must be unique");
321 }
322
323 #[test]
324 fn yolov8n_has_no_classes_override_meaning_default_coco() {
325 let entries = catalog_entries();
326 let yolov8n = entries.iter().find(|e| e.name == "yolov8n").expect("yolov8n entry");
327 assert!(yolov8n.classes.is_empty());
328 }
329
330 #[test]
331 fn face_models_carry_an_explicit_small_class_list() {
332 let entries = catalog_entries();
333 let face_model = entries
334 .iter()
335 .find(|e| e.name == "yolov8n-face")
336 .expect("yolov8n-face entry");
337 assert_eq!(face_model.classes, vec!["face".to_string()]);
338 }
339
340 #[test]
341 fn osnet_carries_an_explicit_non_square_input_size_override() {
342 let entries = catalog_entries();
343 let osnet = entries
344 .iter()
345 .find(|e| e.name == "osnet_x0_25")
346 .expect("osnet_x0_25 entry");
347 assert_eq!(osnet.input_size, Some([128, 256]));
348 }
349
350 #[test]
351 fn every_entry_resolves_to_a_valid_preprocessing_preset() {
352 let entries = catalog_entries();
353 for entry in &entries {
354 let preset = entry
355 .preprocessing_preset
356 .as_deref()
357 .unwrap_or_else(|| panic!("entry '{}' has no preprocessing_preset", entry.name));
358 PreprocessingPreset::from_name(preset).unwrap_or_else(|_| {
359 panic!(
360 "entry '{}' has an invalid preprocessing preset '{}'",
361 entry.name, preset
362 )
363 });
364 }
365 }
366}