From f3d2b8d72ec7ac2d71b6ff106f19b80b8442ed48 Mon Sep 17 00:00:00 2001 From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com> Date: Fri, 22 May 2026 19:29:13 +0000 Subject: [PATCH] feat: add ML model prediction service Co-Authored-By: Claude Opus 4.7 --- apps/ml-service/model.py | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 apps/ml-service/model.py diff --git a/apps/ml-service/model.py b/apps/ml-service/model.py new file mode 100644 index 0000000..26dd96a --- /dev/null +++ b/apps/ml-service/model.py @@ -0,0 +1,70 @@ +from io import BytesIO +import os +from pathlib import Path + +import numpy as np +from PIL import Image, UnidentifiedImageError +import tensorflow as tf + + +LABELS = ["Bercak Daun", "Hawar Daun", "Karat Daun", "Daun Sehat"] +SERVICE_NAME = "zeavis-ml-service" +SERVICE_VERSION = "0.1.0" + + +class ImageDecodeError(ValueError): + pass + + +class ModelService: + def __init__(self) -> None: + self.input_size = int(os.getenv("MODEL_INPUT_SIZE", "224")) + self.model_path = self._resolve_model_path(os.getenv("MODEL_PATH", "../../Machine_Learning/best_model/best_model.keras")) + self.model: tf.keras.Model | None = None + self.load_error: str | None = None + + def _resolve_model_path(self, model_path: str) -> Path: + path = Path(model_path) + if path.is_absolute(): + return path + return (Path(__file__).resolve().parent / path).resolve() + + @property + def model_loaded(self) -> bool: + return self.model is not None + + def load(self) -> None: + try: + self.model = tf.keras.models.load_model(self.model_path, compile=False) + self.load_error = None + except Exception as exc: + self.model = None + self.load_error = str(exc) + + def preprocess(self, image_bytes: bytes) -> np.ndarray: + try: + image = Image.open(BytesIO(image_bytes)).convert("RGB") + except (UnidentifiedImageError, OSError) as exc: + raise ImageDecodeError("Uploaded file is not a valid image") from exc + + image = image.resize((self.input_size, self.input_size)) + image_array = np.asarray(image, dtype=np.float32) + return np.expand_dims(image_array, axis=0) + + def predict(self, image_bytes: bytes) -> tuple[str, float, dict[str, float]]: + if self.model is None: + raise RuntimeError("Model is not loaded") + + batch = self.preprocess(image_bytes) + raw_predictions = self.model.predict(batch, verbose=0)[0] + probabilities_array = np.asarray(raw_predictions, dtype=np.float32) + top_index = int(np.argmax(probabilities_array)) + probabilities = { + label: float(probabilities_array[index]) + for index, label in enumerate(LABELS) + } + + return LABELS[top_index], float(probabilities_array[top_index]), probabilities + + +model_service = ModelService()