File size: 4,284 Bytes
b585b4b
80ebf7c
b585b4b
 
 
80ebf7c
 
 
 
 
 
 
 
b585b4b
 
80ebf7c
 
 
 
 
 
b585b4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80ebf7c
b585b4b
80ebf7c
 
b585b4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80ebf7c
b585b4b
 
 
 
 
 
 
 
 
 
80ebf7c
 
b585b4b
80ebf7c
 
f92b89b
80ebf7c
 
b585b4b
 
80ebf7c
 
f92b89b
80ebf7c
 
b585b4b
80ebf7c
b585b4b
 
80ebf7c
 
 
b585b4b
80ebf7c
 
b585b4b
80ebf7c
 
b585b4b
80ebf7c
b585b4b
80ebf7c
b585b4b
 
80ebf7c
 
b585b4b
80ebf7c
b585b4b
 
80ebf7c
b585b4b
 
 
 
 
 
80ebf7c
 
b585b4b
 
 
80ebf7c
 
 
7b5b03a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""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)