foto / foto_verify.py
Hurum Maksora Tohfa
Add files via upload
45b1715 unverified
Raw
History Blame Contribute Delete
6.27 kB
"""Verification judges: Claude / OpenAI (vision) and DeepSeek (text-only),
sharing the specificity-scaled prompt from the paper benchmark."""
import base64
import io
import json
import time
MODELS = {
"Claude Sonnet 4.6": dict(provider="anthropic", model="claude-sonnet-4-6",
vision=True, price=(3.0, 15.0),
key_url="https://console.anthropic.com/settings/keys",
key_label="Anthropic API Key"),
"Claude Haiku 4.5": dict(provider="anthropic",
model="claude-haiku-4-5-20251001",
vision=True, price=(1.0, 5.0),
key_url="https://console.anthropic.com/settings/keys",
key_label="Anthropic API Key"),
"OpenAI GPT-4o": dict(provider="openai", model="gpt-4o",
vision=True, price=(2.5, 10.0),
key_url="https://platform.openai.com/api-keys",
key_label="OpenAI API Key"),
"DeepSeek (text-only)": dict(provider="deepseek", model="deepseek-chat",
vision=False, price=(0.27, 1.1),
key_url="https://platform.deepseek.com/api_keys",
key_label="DeepSeek API Key"),
}
PROMPT = """A researcher is searching for a published scientific figure with \
this query:
"{query}"
{materials_line}
Caption: {caption}
Judge the candidate against what the query actually specifies. If the query \
is broad or vague, any figure genuinely showing what it describes counts as \
a match; do not demand details the query never mentions. If the query \
specifies particulars (quantities, axes, plot type, what is compared), the \
figure must show them. Science match is the dominant factor: a figure on \
the wrong scientific topic should never score above 0.4.{sketch_line}
Respond JSON only:
{{"match": true|false, "confidence": 0.0-1.0, \
"what_is_plotted": "<one sentence>", "reason": "<one sentence>"}}"""
M_VISION = "Below is the candidate figure image together with its caption."
M_TEXT = "Only the figure's caption is available (no image)."
M_SKETCH = ("\nThe researcher also provided a rough sketch of the figure "
"they remember (first image); weigh structural similarity to it.")
def _jpeg_b64(path_or_bytes, max_dim=1024):
from PIL import Image
if isinstance(path_or_bytes, (bytes, bytearray)):
im = Image.open(io.BytesIO(path_or_bytes))
else:
im = Image.open(path_or_bytes)
im = im.convert("RGB")
im.thumbnail((max_dim, max_dim))
buf = io.BytesIO()
im.save(buf, format="JPEG", quality=80)
return base64.standard_b64encode(buf.getvalue()).decode()
def _parse(text):
obj = json.loads(text[text.index("{"):text.rindex("}") + 1])
return {"match": bool(obj.get("match")),
"confidence": float(obj.get("confidence", 0.0)),
"what_is_plotted": str(obj.get("what_is_plotted", ""))[:300],
"reason": str(obj.get("reason", ""))[:300]}
class Judge:
def __init__(self, model_label, api_key):
cfg = MODELS[model_label]
self.cfg = cfg
self.calls = 0
if cfg["provider"] == "anthropic":
from anthropic import Anthropic
self.client = Anthropic(api_key=api_key)
else:
from openai import OpenAI
base = "https://api.deepseek.com" \
if cfg["provider"] == "deepseek" else None
self.client = OpenAI(api_key=api_key, base_url=base)
def est_cost(self):
i, o = self.cfg["price"]
per = (1300 if self.cfg["vision"] else 500) / 1e6 * i + 80 / 1e6 * o
return self.calls * per
def judge(self, query, caption, image_path=None, sketch_bytes=None,
retries=4):
vision = self.cfg["vision"] and image_path is not None
prompt = PROMPT.format(
query=query,
materials_line=M_VISION if vision else M_TEXT,
caption=(caption or "")[:1500],
sketch_line=M_SKETCH if (sketch_bytes and vision) else "")
for attempt in range(retries):
try:
self.calls += 1
if self.cfg["provider"] == "anthropic":
content = []
if sketch_bytes and vision:
content.append({"type": "image", "source": {
"type": "base64", "media_type": "image/jpeg",
"data": _jpeg_b64(sketch_bytes)}})
if vision:
content.append({"type": "image", "source": {
"type": "base64", "media_type": "image/jpeg",
"data": _jpeg_b64(image_path)}})
content.append({"type": "text", "text": prompt})
resp = self.client.messages.create(
model=self.cfg["model"], max_tokens=300,
messages=[{"role": "user", "content": content}])
return _parse(resp.content[0].text)
else:
if vision:
content = []
if sketch_bytes:
content.append({"type": "image_url", "image_url": {
"url": "data:image/jpeg;base64,"
+ _jpeg_b64(sketch_bytes)}})
content.append({"type": "image_url", "image_url": {
"url": "data:image/jpeg;base64,"
+ _jpeg_b64(image_path)}})
content.append({"type": "text", "text": prompt})
else:
content = prompt
resp = self.client.chat.completions.create(
model=self.cfg["model"], max_tokens=300,
messages=[{"role": "user", "content": content}])
return _parse(resp.choices[0].message.content)
except Exception:
if attempt == retries - 1:
return None
time.sleep(4 * (attempt + 1))