fix(inference): METADATA-35 fit Call 1/2 images to 1024 long edge
Downscale processed PNGs only on the inference send path (keep alpha, never upscale). Stored inventory/Woo blobs unchanged. Adds Pillow + unit/e2e-style tests; cite tests on METADATA-35.
This commit is contained in:
+45
-11
@@ -5,6 +5,7 @@ Cites METADATA-35..115, SETTINGS-30/45/90.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -13,12 +14,53 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from PIL import Image
|
||||
|
||||
from .db import get_connection
|
||||
from .crypto import decrypt
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
# METADATA-35: Call 1 (and Call 2) send clean processed photos at 1024 px long edge or less.
|
||||
_INFER_MAX_LONG_EDGE = 1024
|
||||
|
||||
|
||||
def fit_long_edge_png(png_bytes: bytes, max_long_edge: int = _INFER_MAX_LONG_EDGE) -> bytes:
|
||||
"""Downscale a PNG so max(width, height) <= max_long_edge. Never upscale. Keep alpha.
|
||||
|
||||
Used only for the inference request payload; stored processed_blob / Woo media stay full size.
|
||||
Unreadable or non-image bytes are returned unchanged so a bad blob cannot crash Call 1.
|
||||
"""
|
||||
if not png_bytes or max_long_edge < 1:
|
||||
return png_bytes
|
||||
try:
|
||||
im = Image.open(io.BytesIO(png_bytes))
|
||||
im.load()
|
||||
except Exception:
|
||||
return png_bytes
|
||||
w, h = im.size
|
||||
long_edge = max(w, h)
|
||||
if long_edge <= max_long_edge:
|
||||
# Already within budget; re-encode only if we need a stable PNG (caller always base64s).
|
||||
return png_bytes
|
||||
scale = max_long_edge / float(long_edge)
|
||||
nw = max(1, int(round(w * scale)))
|
||||
nh = max(1, int(round(h * scale)))
|
||||
# Preserve alpha: convert palette/etc. to RGBA when needed, LANCZOS downscale.
|
||||
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||
im = im.convert("RGBA")
|
||||
out = im.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, format="PNG", optimize=True)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _image_part_from_processed(png_bytes: bytes) -> dict:
|
||||
"""Build one OpenAI-style image_url part from a processed PNG (METADATA-35 sized)."""
|
||||
fitted = fit_long_edge_png(png_bytes)
|
||||
b64 = base64.b64encode(fitted).decode()
|
||||
return {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
|
||||
|
||||
CALL1_SEED = (
|
||||
"Classify the slab's species, wood categories, edge type, and figure from the supplied "
|
||||
"photos, choosing ONLY from the supplied synced taxonomy lists (invent no category, term, "
|
||||
@@ -203,17 +245,12 @@ async def call1(slab_id: str) -> dict:
|
||||
client = InferenceClient(base, key, model)
|
||||
act = _active_prompt(conn, "call1")
|
||||
prompt_text = act[1] if act else CALL1_SEED
|
||||
# Build vision message with processed photos (1024px) + taxonomy lists.
|
||||
# Build vision message with processed photos (METADATA-35: long edge <= 1024) + taxonomy.
|
||||
photos = conn.execute(
|
||||
"SELECT processed_blob FROM slab_photos WHERE slab_id=? AND processed_blob IS NOT NULL ORDER BY seq LIMIT 8",
|
||||
(slab_id,),
|
||||
).fetchall()
|
||||
image_parts = []
|
||||
for p in photos:
|
||||
image_parts.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/png;base64,{base64.b64encode(p[0]).decode()}"},
|
||||
})
|
||||
image_parts = [_image_part_from_processed(p[0]) for p in photos]
|
||||
tax = _taxonomy_lists(conn)
|
||||
text = (f"{prompt_text}\n\nSYNCED TAXONOMY (only legal choices):\n{json.dumps(tax)}\n\n"
|
||||
f"SKU={row['sku']} length_in={row['length_in']} thickness_in={row['thickness_in']}")
|
||||
@@ -359,10 +396,7 @@ async def call2(slab_id: str) -> dict:
|
||||
"SELECT processed_blob FROM slab_photos WHERE slab_id=? AND processed_blob IS NOT NULL ORDER BY seq LIMIT 8",
|
||||
(slab_id,),
|
||||
).fetchall()
|
||||
image_parts = [
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64.b64encode(p[0]).decode()}"}}
|
||||
for p in photos
|
||||
]
|
||||
image_parts = [_image_part_from_processed(p[0]) for p in photos]
|
||||
brand = conn.execute("SELECT value FROM settings WHERE key='brand_voice'").fetchone()
|
||||
geo = conn.execute("SELECT value FROM settings WHERE key='geo_context'").fetchone()
|
||||
text = (f"{prompt_text}\n\nDIMENSION FACT BLOCK (exact, invent no numbers):\n"
|
||||
|
||||
Reference in New Issue
Block a user