""" domain/interfaces/services/model_service.py ──────────────────────────────────────────── ModelService — abstract contract for AI model inference. Implementations include: • MockModelService (testing / local dev, no GPU needed) • GANVGTLNetService (production: GAN + VGTL-Net on GPU) """ from __future__ import annotations from abc import ABC, abstractmethod import numpy as np from src.domain.entities.prediction import BPPrediction class ModelService(ABC): """ AI model inference contract. Lifecycle: await service.load_model() → loads weights into memory prediction = await service.predict(ppg_signal_id, segments) # repeat predict() as needed """ @abstractmethod async def load_model(self) -> None: """ Load model weights and prepare for inference. This is intentionally async to allow loading from remote storage (e.g. Hugging Face Hub, S3) without blocking the event loop. Should be idempotent — calling load_model() on an already-loaded service must be a safe no-op. """ ... @abstractmethod async def predict( self, ppg_signal_id: str, segments: np.ndarray, ) -> BPPrediction: """ Run blood pressure inference on preprocessed PPG segments. Args: ppg_signal_id: UUID of the source PPGSignal — stored in the result. segments: 2-D NumPy array of shape ``(n_segments, window_size)`` produced by SignalProcessor.process(). Returns: BPPrediction domain entity containing predicted SBP, DBP, inference timing, and model version. Raises: PredictionOutOfRangeError: If the model output is physiologically impossible (handled by BPPrediction.validate()). RuntimeError: If the model has not been loaded (load_model() not called). """ ... @abstractmethod def is_loaded(self) -> bool: """Return ``True`` if model weights are loaded and ready for inference.""" ... @property @abstractmethod def model_version(self) -> str: """Version string identifying this model (stored in BPPrediction).""" ...