Spaces:
Running
Running
| """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) | |