""" 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*(?PYES|NO|PARTIAL)\s*\]?\s*" r"DISCREP\w*:\s*\[?\s*(?P.*?)\s*\]?\s*" r"CONFIDENCE:\s*\[?\s*(?PHigh|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"), }