Crison11 commited on
Commit
22909ae
·
1 Parent(s): f8c2cb1

feat: add gradio ai-image detector demo and clean repo ignores

Browse files
Files changed (4) hide show
  1. .gitignore +5 -0
  2. README.md +27 -0
  3. app.py +185 -0
  4. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .agents/
2
+ .codex/
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
README.md CHANGED
@@ -10,3 +10,30 @@ pinned: false
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ ## Demo Description
15
+
16
+ This Space provides a simple AI-image detection demo:
17
+
18
+ 1. Upload one image in the Gradio UI.
19
+ 2. The app calls an external vision-capable LLM API (OpenAI-compatible).
20
+ 3. It returns:
21
+ - `classification`: `REAL` / `AI_GENERATED` / `UNSURE`
22
+ - `confidence`: `0-100`
23
+ - `signals`: key visual clues
24
+ - `summary`: short explanation
25
+
26
+ ## Environment Variables (Space Secrets)
27
+
28
+ Set the following in your Space settings:
29
+
30
+ - `OPENAI_API_KEY` (required)
31
+ - `OPENAI_MODEL` (optional, default: `gpt-4.1-mini`)
32
+ - `OPENAI_BASE_URL` (optional, for OpenAI-compatible third-party services)
33
+
34
+ ## Local Run
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ python app.py
39
+ ```
app.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+ import gradio as gr
8
+ from openai import OpenAI
9
+ from PIL import Image
10
+
11
+
12
+ SYSTEM_PROMPT = """
13
+ You are an expert in image forensics.
14
+ Your task is to assess whether an image is likely a real photo or AI-generated.
15
+
16
+ Return strict JSON only:
17
+ {
18
+ "classification": "REAL|AI_GENERATED|UNSURE",
19
+ "confidence": 0-100,
20
+ "signals": ["short signal 1", "short signal 2"],
21
+ "summary": "one concise paragraph"
22
+ }
23
+
24
+ Rules:
25
+ - Base your judgment on visible artifacts and coherence.
26
+ - Do not claim certainty unless confidence is high.
27
+ - If evidence is mixed, use UNSURE.
28
+ """.strip()
29
+
30
+
31
+ def _build_client() -> OpenAI:
32
+ api_key = os.getenv("OPENAI_API_KEY")
33
+ if not api_key:
34
+ raise ValueError("Missing OPENAI_API_KEY. Please configure it in Space Secrets.")
35
+
36
+ base_url = os.getenv("OPENAI_BASE_URL", "").strip()
37
+ if base_url:
38
+ return OpenAI(api_key=api_key, base_url=base_url)
39
+ return OpenAI(api_key=api_key)
40
+
41
+
42
+ def _image_to_data_url(image: Image.Image) -> str:
43
+ buffer = io.BytesIO()
44
+ image.convert("RGB").save(buffer, format="JPEG", quality=95)
45
+ b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
46
+ return f"data:image/jpeg;base64,{b64}"
47
+
48
+
49
+ def _extract_output_text(response: Any) -> str:
50
+ output_text = getattr(response, "output_text", None)
51
+ if isinstance(output_text, str) and output_text.strip():
52
+ return output_text.strip()
53
+
54
+ chunks: list[str] = []
55
+ for item in getattr(response, "output", []) or []:
56
+ for content in getattr(item, "content", []) or []:
57
+ text = getattr(content, "text", None)
58
+ if isinstance(text, str) and text.strip():
59
+ chunks.append(text.strip())
60
+ return "\n".join(chunks).strip()
61
+
62
+
63
+ def _safe_parse_json(text: str) -> dict[str, Any] | None:
64
+ if not text:
65
+ return None
66
+
67
+ try:
68
+ data = json.loads(text)
69
+ if isinstance(data, dict):
70
+ return data
71
+ except json.JSONDecodeError:
72
+ pass
73
+
74
+ start = text.find("{")
75
+ end = text.rfind("}")
76
+ if start != -1 and end != -1 and end > start:
77
+ try:
78
+ data = json.loads(text[start : end + 1])
79
+ if isinstance(data, dict):
80
+ return data
81
+ except json.JSONDecodeError:
82
+ return None
83
+ return None
84
+
85
+
86
+ def _format_result(data: dict[str, Any]) -> str:
87
+ classification = str(data.get("classification", "UNSURE")).upper()
88
+ confidence = data.get("confidence", "N/A")
89
+ signals = data.get("signals", [])
90
+ summary = str(data.get("summary", "")).strip()
91
+
92
+ if not isinstance(signals, list):
93
+ signals = [str(signals)]
94
+
95
+ signal_lines = "\n".join(f"- {str(s)}" for s in signals[:8]) if signals else "- (none)"
96
+
97
+ return (
98
+ f"### Analysis Result\n"
99
+ f"- **Classification**: `{classification}`\n"
100
+ f"- **Confidence**: `{confidence}`\n"
101
+ f"- **Key Signals**:\n{signal_lines}\n\n"
102
+ f"### Summary\n{summary or '(empty)'}\n\n"
103
+ f"> Note: This output is for demo and decision-support purposes only, not a professional forensic conclusion."
104
+ )
105
+
106
+
107
+ def analyze_image(image: Image.Image | None, extra_instruction: str) -> tuple[str, str]:
108
+ if image is None:
109
+ return "Please upload an image first.", ""
110
+
111
+ try:
112
+ client = _build_client()
113
+ model = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
114
+
115
+ data_url = _image_to_data_url(image)
116
+ user_prompt = (
117
+ "Analyze this image and decide whether it is REAL, AI_GENERATED, or UNSURE. "
118
+ "Return strict JSON only."
119
+ )
120
+ if extra_instruction and extra_instruction.strip():
121
+ user_prompt += f"\nAdditional user instruction: {extra_instruction.strip()}"
122
+
123
+ response = client.responses.create(
124
+ model=model,
125
+ input=[
126
+ {"role": "system", "content": [{"type": "input_text", "text": SYSTEM_PROMPT}]},
127
+ {
128
+ "role": "user",
129
+ "content": [
130
+ {"type": "input_text", "text": user_prompt},
131
+ {"type": "input_image", "image_url": data_url},
132
+ ],
133
+ },
134
+ ],
135
+ )
136
+
137
+ raw_text = _extract_output_text(response)
138
+ parsed = _safe_parse_json(raw_text)
139
+
140
+ if parsed is None:
141
+ return (
142
+ "The model returned non-JSON content. Please inspect the raw output and adjust the prompt or model.",
143
+ raw_text or "(empty response)",
144
+ )
145
+ return _format_result(parsed), json.dumps(parsed, ensure_ascii=False, indent=2)
146
+ except Exception as exc:
147
+ return f"Request failed: `{type(exc).__name__}: {exc}`", ""
148
+
149
+
150
+ with gr.Blocks(title="AI Image Detector Demo") as demo:
151
+ gr.Markdown(
152
+ """
153
+ # AI Image Detector (Gradio + GPT API)
154
+
155
+ Upload an image and call an external vision-capable LLM API to judge whether
156
+ the image is likely a real photo or AI-generated.
157
+
158
+ Before running, configure these Hugging Face Space Secrets:
159
+ - `OPENAI_API_KEY` (required)
160
+ - `OPENAI_MODEL` (optional, default: `gpt-4.1-mini`)
161
+ - `OPENAI_BASE_URL` (optional, for compatible third-party endpoints)
162
+ """
163
+ )
164
+
165
+ with gr.Row():
166
+ image_input = gr.Image(type="pil", label="Upload Image")
167
+ prompt_input = gr.Textbox(
168
+ label="Additional Instruction (Optional)",
169
+ placeholder="Example: Focus on skin texture, finger structure, and text regions.",
170
+ lines=6,
171
+ )
172
+
173
+ run_btn = gr.Button("Start Analysis", variant="primary")
174
+ result_md = gr.Markdown(label="Structured Result")
175
+ raw_json = gr.Code(label="Raw Model JSON", language="json")
176
+
177
+ run_btn.click(
178
+ fn=analyze_image,
179
+ inputs=[image_input, prompt_input],
180
+ outputs=[result_md, raw_json],
181
+ )
182
+
183
+
184
+ if __name__ == "__main__":
185
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=6.9.0
2
+ openai>=1.40.0
3
+ Pillow>=10.0.0