"""Validated single-image input for Decisions. Images stay out of the text state."""
import base64
import binascii
import io
from PIL import Image, UnidentifiedImageError
from decisions_api import ApiError
IMG_MARK = "<
>"
IMG_BLOCK = "<|vision_start|><|image_pad|><|vision_end|>"
MAX_IMAGE_BYTES = 4 * 1024 * 1024
MAX_IMAGE_PIXELS = 12_000_000
def image_input(body):
"""Accept base64 or a data URI; never read server paths or fetch remote URLs."""
value = body.get("image_data")
if value is None:
return None
if not isinstance(value, str) or not value:
raise ApiError(400, "image_data must be one base64 image or image data URI")
if value.startswith("data:"):
header, sep, value = value.partition(",")
if not sep or not header.startswith("data:image/") or not header.endswith(";base64"):
raise ApiError(400, "image_data must use an image base64 data URI")
if len(value) > 4 * ((MAX_IMAGE_BYTES + 2) // 3):
raise ApiError(413, "image_data exceeds 4 MiB decoded")
try:
raw = base64.b64decode(value, validate=True)
if len(raw) > MAX_IMAGE_BYTES:
raise ApiError(413, "image_data exceeds 4 MiB decoded")
with Image.open(io.BytesIO(raw)) as im:
if im.format not in {"JPEG", "PNG", "WEBP"} or getattr(im, "n_frames", 1) != 1:
raise ApiError(400, "image_data must be a single JPEG, PNG or WebP image")
if im.width * im.height > MAX_IMAGE_PIXELS:
raise ApiError(413, "image_data exceeds 12 million pixels")
im.verify()
except (binascii.Error, ValueError, OSError, UnidentifiedImageError, Image.DecompressionBombError) as e:
raise ApiError(400, "image_data is not a valid base64 image") from e
return value
def image_premise(premise):
if premise.count(IMG_MARK) > 1 or "<|image_pad|>" in premise or "<|vision_start|>" in premise:
raise ApiError(400, "Use at most one <
> marker in the image premise")
return premise.replace(IMG_MARK, IMG_BLOCK) if IMG_MARK in premise else premise.rstrip() + " " + IMG_BLOCK