77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
"""
|
|
Optional third-party AI verification, layered on top of the core
|
|
feature-matching pipeline: sends the winning template's original photo and
|
|
the user's original upload to an external vision-LLM endpoint that runs a
|
|
detailed QC-style comparison (flowers, vase, ribbon, composition) and
|
|
returns a MATCH/DISCREPANCIES/CONFIDENCE verdict plus free-text description.
|
|
|
|
This never affects the core match result -- if the endpoint is slow, down,
|
|
or unreachable (it's a Cloudflare tunnel, which can go stale), the caller
|
|
is expected to treat any exception here as a soft failure.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import requests
|
|
|
|
import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_VERDICT_RE = re.compile(
|
|
# DISCREP\w* rather than a literal "DISCREPANCIES" -- the LLM behind
|
|
# the endpoint doesn't reliably spell it the same way every time
|
|
# ("DISCREPANCIES" vs "DISCREPENCIES" have both been observed), and a
|
|
# missed match here silently dumps the whole raw block into the UI.
|
|
r"MATCH:\s*\[?\s*(?P<match>YES|NO|PARTIAL)\s*\]?\s*"
|
|
r"DISCREP\w*:\s*\[?\s*(?P<discrepancies>.*?)\s*\]?\s*"
|
|
r"CONFIDENCE:\s*\[?\s*(?P<confidence>High|Medium|Low)\s*\]?",
|
|
re.IGNORECASE | re.DOTALL,
|
|
)
|
|
|
|
|
|
def _parse_result_text(text: str) -> dict:
|
|
"""The endpoint's `result` field is free text with an embedded
|
|
MATCH/DISCREPANCIES/CONFIDENCE block, sometimes followed by a prose
|
|
description, sometimes not. Pull out the structured bits; whatever's
|
|
left over (if anything) is the description."""
|
|
text = text or ""
|
|
m = _VERDICT_RE.search(text)
|
|
|
|
if not m:
|
|
return {"match": None, "confidence": None, "discrepancies": None,
|
|
"description": text.strip()}
|
|
|
|
return {
|
|
"match": m.group("match").upper(),
|
|
"confidence": m.group("confidence").capitalize(),
|
|
"discrepancies": m.group("discrepancies").strip() or "None",
|
|
"description": text[m.end():].strip(),
|
|
}
|
|
|
|
|
|
def verify_images(reference_path: str, actual_path: str) -> dict:
|
|
"""reference_path = the matched template's original photo (IMAGE 1),
|
|
actual_path = the user's original upload (IMAGE 2)."""
|
|
with open(reference_path, "rb") as f1, open(actual_path, "rb") as f2:
|
|
files = {
|
|
"image1": (os.path.basename(reference_path), f1, "image/png"),
|
|
"image2": (os.path.basename(actual_path), f2, "image/jpeg"),
|
|
}
|
|
response = requests.post(config.VERIFY_ENDPOINT_URL, files=files,
|
|
timeout=config.VERIFY_TIMEOUT_SECONDS)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
parsed = _parse_result_text(data.get("result", ""))
|
|
return {
|
|
"match": parsed["match"],
|
|
"confidence": parsed["confidence"],
|
|
"discrepancies": parsed["discrepancies"],
|
|
"description": parsed["description"],
|
|
"raw_result": data.get("result"),
|
|
"pixel_precheck": data.get("pixel_precheck"),
|
|
}
|