"""Spot the AI Receipt — 2AFC game by Scam.AI. Simplified flow: clicking either button atomically scores the guess AND loads the next pair. No separate 'Next round' button. Fewer outputs, fewer state updates, faster perceived latency. """ import random from pathlib import Path import gradio as gr ROOT = Path(__file__).parent REAL_IMGS = [str(p) for p in sorted((ROOT / "data" / "real").glob("*"))] AI_IMGS = [str(p) for p in sorted((ROOT / "data" / "ai").glob("*"))] HUMAN_F1 = 0.852 CLAUDE_F1 = 0.975 GEMINI_F1 = 0.890 def play(guess, state): """One round-trip: score current guess + serve next pair.""" state = dict(state) if state else {"correct": 0, "total": 0, "ai_side": None} # 1. Score the previous guess (if any) feedback_md = "" if guess and state.get("ai_side"): correct = state["ai_side"] == guess state["correct"] = state.get("correct", 0) + (1 if correct else 0) state["total"] = state.get("total", 0) + 1 winner_letter = "A" if state["ai_side"] == "left" else "B" if correct: feedback_md = f"### ✅ Correct! **{winner_letter}** was the AI-generated receipt." else: feedback_md = f"### ❌ Wrong. **{winner_letter}** was the AI-generated receipt." # 2. Pick next pair ai = random.choice(AI_IMGS) real = random.choice(REAL_IMGS) ai_on_left = random.random() < 0.5 state["ai_side"] = "left" if ai_on_left else "right" left = ai if ai_on_left else real right = real if ai_on_left else ai # 3. Score card total = state.get("total", 0) correct = state.get("correct", 0) if total > 0: score_md = ( f"**Your score: {correct} / {total} ({100*correct/total:.0f}%)** · " f"Trained humans: F1 = {HUMAN_F1} · Claude Sonnet 4: F1 = {CLAUDE_F1}" ) else: score_md = ( "*Pick the AI-generated receipt. After each click, you'll see " "the answer and the next pair.*" ) # 4. After 10 rounds, append a CTA if total >= 10: feedback_md += ( "\n\n---\n\n" "### Done with 10 rounds 🎉\n" "AI-generated receipts are visually realistic. The forensic " "signal is in **arithmetic incoherence** — invisible to the eye, " "trivial for LLMs to verify.\n\n" "👉 **Production-grade document fraud detection: " "[scam.ai](https://www.scam.ai)**" ) return left, right, state, feedback_md, score_md with gr.Blocks(title="Spot the AI Receipt — Scam.AI") as demo: gr.Markdown( "# 🧾 Spot the AI Receipt\n" "*One of these receipts is real, one was fully synthesized by " "GPT-4o + GPT-Image-1. Pick the AI one.*\n\n" "*Built by [Scam.AI](https://www.scam.ai) · Data: " "[gpt4o-receipt](https://huggingface.co/datasets/Scam-AI/gpt4o-receipt) " "+ [CORD-v2](https://huggingface.co/datasets/naver-clova-ix/cord-v2)*" ) state = gr.State({"correct": 0, "total": 0, "ai_side": None}) score = gr.Markdown() feedback = gr.Markdown() with gr.Row(): with gr.Column(): img_l = gr.Image(label="Receipt A", interactive=False, height=400) btn_l = gr.Button("👉 A is the AI", variant="primary", size="lg") with gr.Column(): img_r = gr.Image(label="Receipt B", interactive=False, height=400) btn_r = gr.Button("👉 B is the AI", variant="primary", size="lg") # All clicks call the same `play` function with different guess label btn_l.click( lambda s: play("left", s), inputs=state, outputs=[img_l, img_r, state, feedback, score], queue=True, ) btn_r.click( lambda s: play("right", s), inputs=state, outputs=[img_l, img_r, state, feedback, score], queue=True, ) # Initial load — no guess demo.load( lambda s: play(None, s), inputs=state, outputs=[img_l, img_r, state, feedback, score], ) # Queue config: allow parallel processing demo.queue(default_concurrency_limit=10, max_size=50) if __name__ == "__main__": demo.launch(theme=gr.themes.Soft(primary_hue="blue"), ssr_mode=False)