"""Image-decoding utilities for the HTTP boundary. The ML package's ``inference/image_loader.py`` reads from disk; the API receives bytes in memory from a multipart upload. This module bridges the two: it decodes raw bytes and runs them through the *same* ``preprocess_image_tensor`` the training pipeline uses, so train/serve parity is preserved by construction. TensorFlow imports are deferred until first call to keep app import cheap (e.g. when running ``ruff`` or constructing the app for tests with stub predictors). """ from __future__ import annotations from typing import Any ALLOWED_CONTENT_TYPES: frozenset[str] = frozenset( { "image/jpeg", "image/jpg", "image/png", "image/webp", "image/bmp", } ) class ImageDecodeError(ValueError): """Raised when uploaded bytes are not a recognisable image.""" def bytes_to_tensor(image_bytes: bytes) -> Any: """Decode an in-memory image into a model-ready tensor. Args: image_bytes: Raw bytes from a multipart upload (JPEG/PNG/WebP/BMP). Returns: ``tf.Tensor`` of shape ``[299, 299, 3]``, dtype ``float32``, with the InceptionV3 normalisation applied — i.e. exactly what ``CaptionPredictor.predict_tensor`` expects. Raises: ImageDecodeError: If the bytes can't be decoded as an image. """ import tensorflow as tf from captioning.preprocessing.image import preprocess_image_tensor try: decoded = tf.io.decode_image( image_bytes, channels=3, expand_animations=False, ) except (tf.errors.InvalidArgumentError, tf.errors.UnknownError) as exc: raise ImageDecodeError(f"Could not decode image bytes: {exc}") from exc return preprocess_image_tensor(decoded)