use super::ModelService;
use anyhow::Result;
use indicatif::ProgressBar;
use std::path::Path;
use tokio::fs;
impl ModelService {
#[cfg(feature = "model-download")]
pub(super) async fn embed_class_names_metadata(
&self,
model_path: &Path,
class_names: &[String],
progress: Option<&ProgressBar>,
) -> Result<()> {
if class_names.is_empty() {
return Ok(());
}
let python = match self.find_python() {
Ok(python) => python,
Err(e) => {
tracing::warn!(
"Skipping embedding \"class_names\" metadata into {}: {e}. \
object-detector will report zero detections for this model until it's re-pulled \
with Python 3 available.",
model_path.display()
);
return Ok(());
}
};
if let Some(pb) = progress {
pb.set_message("Embedding class names into model metadata...");
}
let script = include_str!("../../../scripts/embed_class_names.py");
let script_path = std::env::temp_dir().join("mecha10_embed_class_names.py");
fs::write(&script_path, script).await?;
let class_names_json = serde_json::to_string(class_names)?;
let class_names_path = model_path.with_file_name("class_names.json");
fs::write(&class_names_path, &class_names_json).await?;
let output_result = tokio::process::Command::new(&python)
.arg(&script_path)
.arg(model_path)
.arg(&class_names_path)
.arg(model_path)
.output()
.await?;
let _ = fs::remove_file(&script_path).await;
let _ = fs::remove_file(&class_names_path).await;
if !output_result.status.success() {
let stderr = String::from_utf8_lossy(&output_result.stderr);
tracing::warn!(
"Skipping embedding \"class_names\" metadata into {}: {}\n\nTip: install with 'pip install onnx' \
to let object-detector see this model's classes.",
model_path.display(),
stderr.trim()
);
if let Some(pb) = progress {
pb.set_message("⚠️ Skipped embedding class names (see logs)");
}
return Ok(());
}
if let Some(pb) = progress {
pb.set_message("✅ Embedded class names into model metadata");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::ModelService;
use std::path::PathBuf;
fn fixture_model_bytes() -> &'static [u8] {
include_bytes!("../../../tests/fixtures/tiny.onnx")
}
fn service(models_dir: PathBuf) -> ModelService {
ModelService::with_models_dir(models_dir).unwrap()
}
async fn skip_if_onnx_unavailable(service: &ModelService) -> bool {
let Ok(python) = service.find_python() else {
return true;
};
let output = tokio::process::Command::new(&python)
.arg("-c")
.arg("import onnx")
.output()
.await;
!matches!(output, Ok(out) if out.status.success())
}
#[tokio::test]
async fn embeds_class_names_matching_resolved_labels() {
let tmp = tempfile::tempdir().unwrap();
let service = service(tmp.path().to_path_buf());
if skip_if_onnx_unavailable(&service).await {
return;
}
let model_path = tmp.path().join("model.onnx");
tokio::fs::write(&model_path, fixture_model_bytes()).await.unwrap();
let class_names = vec!["person".to_string(), "bicycle".to_string(), "car".to_string()];
service
.embed_class_names_metadata(&model_path, &class_names, None)
.await
.unwrap();
let python = service.find_python().unwrap();
let output = tokio::process::Command::new(&python)
.arg("-c")
.arg(format!(
"import onnx, json, sys; m = onnx.load({model_path:?}); \
props = {{p.key: p.value for p in m.metadata_props}}; \
sys.stdout.write(props.get('class_names', ''))"
))
.output()
.await
.unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let raw = String::from_utf8_lossy(&output.stdout);
let parsed: Vec<String> = serde_json::from_str(&raw).expect("class_names metadata must be a JSON string array");
assert_eq!(parsed, class_names);
}
#[tokio::test]
async fn does_not_touch_the_model_when_class_names_is_empty() {
let tmp = tempfile::tempdir().unwrap();
let service = service(tmp.path().to_path_buf());
let model_path = tmp.path().join("model.onnx");
tokio::fs::write(&model_path, fixture_model_bytes()).await.unwrap();
let before = tokio::fs::read(&model_path).await.unwrap();
service
.embed_class_names_metadata(&model_path, &[], None)
.await
.unwrap();
let after = tokio::fs::read(&model_path).await.unwrap();
assert_eq!(
before, after,
"an empty class list must not write a placeholder class_names key"
);
}
}