Users can now attach images in the web chat for Simba to analyze using Ollama's gemma3 vision model. Images are stored in Garage (S3-compatible) and displayed in chat history. Also fixes aerich migration config by extracting TORTOISE_CONFIG into a standalone config/db.py module, removing the stale aerich_config.py, and adding missing MODELS_STATE to migration 3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
import io
|
|
import logging
|
|
|
|
from PIL import Image
|
|
from pillow_heif import register_heif_opener
|
|
|
|
register_heif_opener()
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp", "image/heic", "image/heif"}
|
|
MAX_DIMENSION = 1920
|
|
|
|
|
|
class ImageValidationError(Exception):
|
|
pass
|
|
|
|
|
|
def process_image(file_bytes: bytes, content_type: str) -> tuple[bytes, str]:
|
|
"""Validate, resize, and strip EXIF from an uploaded image.
|
|
|
|
Returns processed bytes and the output content type (always image/jpeg or image/png or image/webp).
|
|
"""
|
|
if content_type not in ALLOWED_TYPES:
|
|
raise ImageValidationError(
|
|
f"Unsupported image type: {content_type}. "
|
|
f"Allowed: JPEG, PNG, WebP, HEIC"
|
|
)
|
|
|
|
img = Image.open(io.BytesIO(file_bytes))
|
|
|
|
# Resize if too large
|
|
width, height = img.size
|
|
if max(width, height) > MAX_DIMENSION:
|
|
ratio = MAX_DIMENSION / max(width, height)
|
|
new_size = (int(width * ratio), int(height * ratio))
|
|
img = img.resize(new_size, Image.LANCZOS)
|
|
logging.info(
|
|
f"Resized image from {width}x{height} to {new_size[0]}x{new_size[1]}"
|
|
)
|
|
|
|
# Strip EXIF by copying pixel data to a new image
|
|
clean_img = Image.new(img.mode, img.size)
|
|
clean_img.putdata(list(img.getdata()))
|
|
|
|
# Convert HEIC/HEIF to JPEG; otherwise keep original format
|
|
if content_type in {"image/heic", "image/heif"}:
|
|
output_format = "JPEG"
|
|
output_content_type = "image/jpeg"
|
|
elif content_type == "image/png":
|
|
output_format = "PNG"
|
|
output_content_type = "image/png"
|
|
elif content_type == "image/webp":
|
|
output_format = "WEBP"
|
|
output_content_type = "image/webp"
|
|
else:
|
|
output_format = "JPEG"
|
|
output_content_type = "image/jpeg"
|
|
|
|
buf = io.BytesIO()
|
|
clean_img.save(buf, format=output_format, quality=85)
|
|
return buf.getvalue(), output_content_type
|