feat: scaffold Rust ML service crate
Create Rust crate skeleton with Cargo.toml, main.rs, and config.rs. Includes package metadata, dependencies (anyhow, axum, image, ndarray, ort, serde, tokio, tower, tracing), and config module with LABELS constants, SERVICE_NAME, SERVICE_VERSION, DEFAULT_INPUT_SIZE, and resolve_model_path function. Config tests verify label order, relative path resolution, and absolute path preservation. Also includes approved spec and implementation plan documents. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b735897dc2
commit
10e890cc42
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "zeavis-ml-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = { version = "0.7", features = ["multipart"] }
|
||||
image = "0.25"
|
||||
ndarray = "0.15"
|
||||
ort = "2.0.0-rc.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "net"] }
|
||||
tower = "0.5"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = "0.3"
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const LABELS: [&str; 4] = ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"];
|
||||
pub const SERVICE_NAME: &str = "zeavis-ml-service";
|
||||
pub const SERVICE_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const DEFAULT_INPUT_SIZE: u32 = 224;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub model_path: PathBuf,
|
||||
pub input_size: u32,
|
||||
}
|
||||
|
||||
pub fn resolve_model_path(base_dir: &Path, model_path: &str) -> PathBuf {
|
||||
let path = PathBuf::from(model_path);
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
base_dir.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn labels_match_training_class_order_with_display_names() {
|
||||
assert_eq!(LABELS, ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_model_path_resolves_from_service_directory() {
|
||||
let base = Path::new("/repo/apps/ml-service");
|
||||
let resolved = resolve_model_path(base, "../../Machine_Learning/model/model.onnx");
|
||||
assert_eq!(resolved, PathBuf::from("/repo/apps/ml-service/../../Machine_Learning/model/model.onnx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_model_path_is_preserved() {
|
||||
let base = Path::new("/repo/apps/ml-service");
|
||||
let resolved = resolve_model_path(base, "/models/model.onnx");
|
||||
assert_eq!(resolved, PathBuf::from("/models/model.onnx"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod config;
|
||||
|
||||
fn main() {
|
||||
println!("zeavis-ml-service");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
# Rust ONNX ML Service Migration Design
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the current Python/FastAPI ML serving runtime with a Rust service that uses Axum and ONNX Runtime while preserving the existing HTTP contract, deployment shape, and model labels. The migration also adds an ONNX conversion path so the Keras/SavedModel training output can produce the model artifact used by the Rust service.
|
||||
|
||||
## Current context
|
||||
|
||||
The existing service lives in `apps/ml-service` and exposes three endpoints:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /metadata`
|
||||
- `POST /predict`
|
||||
|
||||
It loads a Keras model from `MODEL_PATH`, defaults to `../../Machine_Learning/best_model/best_model.keras`, preprocesses uploaded images as RGB resized to `224x224`, sends a float32 NHWC batch to the model, and returns the top label, confidence, and all label probabilities.
|
||||
|
||||
Deployment already expects an `ml` service listening on port `8000`, with API integration configured through `ML_SERVICE_URL`.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Replace the Python serving code fully rather than running Python and Rust side by side.
|
||||
- Use Rust with Axum for the HTTP server.
|
||||
- Use the `ort` crate for ONNX Runtime inference.
|
||||
- Keep the current API contract for `/health`, `/metadata`, and `/predict`.
|
||||
- Keep the current preprocessing behavior: RGB, resize to `224x224`, float32 tensor, NHWC shape `[1, 224, 224, 3]`, no additional normalization.
|
||||
- Keep deployment compatibility: service name `ml`, internal port `8000`.
|
||||
- Add ONNX conversion to the ML export workflow.
|
||||
- Add parity validation as a manual verification command because model artifacts and sample images are not guaranteed to exist in fresh clones or CI.
|
||||
|
||||
## Architecture
|
||||
|
||||
`apps/ml-service` becomes a Rust binary crate. The service is split into small modules:
|
||||
|
||||
- `main.rs`: startup, configuration loading, Axum router, TCP listener.
|
||||
- `config.rs`: environment variables, defaults, labels, service metadata.
|
||||
- `routes.rs`: HTTP handlers and response types.
|
||||
- `model.rs`: ONNX session loading and prediction.
|
||||
- `image.rs`: upload image decoding and preprocessing.
|
||||
- `error.rs`: typed errors mapped to HTTP responses.
|
||||
|
||||
The default model path changes to `../../Machine_Learning/model/model.onnx`. `MODEL_PATH` can still override the path. `MODEL_INPUT_SIZE` defaults to `224`.
|
||||
|
||||
## API contract
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"model_loaded": true
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint still responds even if the model failed to load, with `model_loaded: false`.
|
||||
|
||||
### `GET /metadata`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"service_name": "zeavis-ml-service",
|
||||
"service_version": "0.1.0",
|
||||
"model_path": ".../Machine_Learning/model/model.onnx",
|
||||
"model_loaded": true,
|
||||
"input_size": 224,
|
||||
"labels": ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /predict`
|
||||
|
||||
Accepts multipart form data with field `file`. Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Karat Daun",
|
||||
"confidence": 0.98,
|
||||
"probabilities": {
|
||||
"Bercak Daun": 0.01,
|
||||
"Daun Sehat": 0.0,
|
||||
"Karat Daun": 0.98,
|
||||
"Hawar Daun": 0.01
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data flow
|
||||
|
||||
1. Client uploads an image to `/predict` as multipart field `file`.
|
||||
2. The route validates that a file is present and that the content type is an image when provided.
|
||||
3. `image.rs` decodes the image, converts it to RGB, resizes it to `MODEL_INPUT_SIZE x MODEL_INPUT_SIZE`, casts pixels to `f32`, and creates an NHWC tensor with batch dimension.
|
||||
4. `model.rs` runs the tensor through ONNX Runtime.
|
||||
5. The output vector is mapped to the fixed Indonesian labels.
|
||||
6. The service selects the highest-probability label and returns all probabilities.
|
||||
|
||||
## ONNX export pipeline
|
||||
|
||||
The ML pipeline keeps the current Keras and SavedModel exports, then adds an ONNX output:
|
||||
|
||||
1. Training produces `Machine_Learning/best_model/best_model.keras`.
|
||||
2. `Machine_Learning/save_model.py` continues exporting SavedModel and TFLite.
|
||||
3. A new conversion command or script produces `Machine_Learning/model/model.onnx` from the exported SavedModel or Keras model.
|
||||
4. Documentation explains required Python dependencies and the exact command to regenerate `model.onnx`.
|
||||
|
||||
The ONNX artifact is generated/local like the existing model exports and may not exist in a fresh clone.
|
||||
|
||||
## Parity validation
|
||||
|
||||
Parity validation compares Keras and ONNX predictions for the same sample images. It is a manual verification command, not a required CI test.
|
||||
|
||||
The validation should check:
|
||||
|
||||
- Top-1 label matches.
|
||||
- Probability vectors are numerically close within an explicit tolerance.
|
||||
- The preprocessing used for comparison matches the Rust service: RGB resize, float32 NHWC, no extra normalization.
|
||||
|
||||
If parity fails, the migration should stop until conversion input shape, preprocessing, or output mapping is corrected.
|
||||
|
||||
## Error handling
|
||||
|
||||
The Rust service maps errors to the same behavior as the current Python service:
|
||||
|
||||
- Missing file or non-image upload: `400`.
|
||||
- Invalid image bytes or decode failure: `400`.
|
||||
- Model not loaded: `503`.
|
||||
- Unexpected inference failure: `500`.
|
||||
|
||||
Startup should try to load the model and keep the service alive if loading fails so `/health` and `/metadata` can report `model_loaded: false`.
|
||||
|
||||
## Testing and verification
|
||||
|
||||
Required local verification:
|
||||
|
||||
- `cargo test` from `apps/ml-service`.
|
||||
- `cargo build --release` from `apps/ml-service`.
|
||||
- Manual endpoint checks with `curl` for `/health`, `/metadata`, and `/predict` when `model.onnx` is available.
|
||||
- Manual parity validation when both Keras and ONNX artifacts plus sample images are available.
|
||||
|
||||
The repository has no existing global test suite, so the Rust service checks become the primary verification for this migration.
|
||||
|
||||
## Documentation and deployment updates
|
||||
|
||||
Update documentation so runtime serving no longer describes FastAPI/TensorFlow as the production ML service. Keep Python/TensorFlow documentation for training and export.
|
||||
|
||||
Update:
|
||||
|
||||
- Root `README.md`.
|
||||
- `apps/ml-service/README.md`.
|
||||
- `Machine_Learning/README.md` where export artifacts and ONNX conversion are described.
|
||||
- Dockerfile or deployment files that build the `ml` image.
|
||||
|
||||
Deployment remains compatible with the existing `ml` service and port `8000` expectation.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing the model architecture or class labels.
|
||||
- Retraining the model.
|
||||
- Changing API/backend integration contracts.
|
||||
- Adding GPU acceleration.
|
||||
- Making parity validation mandatory in CI.
|
||||
Reference in New Issue
Block a user