Samsularif commited on
Commit
c47d277
·
verified ·
1 Parent(s): 1007d07

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +105 -0
  2. facecomparison_multi_resume.py +1251 -0
  3. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import tempfile
4
+ import time
5
+ from facecomparison_multi_resume import DeepfakeDetector
6
+ from PIL import Image
7
+
8
+ # ===================================================================
9
+ # 1. MENGAMBIL KUNCI API DARI ENVIRONMENT VARIABLES (SECRETS)
10
+ # Nama-nama ini harus sama persis dengan yang Anda set di Hugging Face Secrets!
11
+ # ===================================================================
12
+
13
+ API_KEYS = {
14
+ "qwen": os.environ.get("OPENROUTER_API_KEY_QWEN"),
15
+ "gpt": os.environ.get("OPENROUTER_API_KEY_GPT"),
16
+ "gemini": os.environ.get("OPENROUTER_API_KEY_GEMINI"),
17
+ "llama": os.environ.get("OPENROUTER_API_KEY_LLAMA"),
18
+ "cohere": os.environ.get("OPENROUTER_API_KEY_COHERE"),
19
+ }
20
+
21
+ MODEL_NAMES = ["qwen", "gpt", "gemini", "llama", "cohere"]
22
+
23
+ # ===================================================================
24
+ # 2. FUNGSI UTAMA UNTUK ANALISIS SATU GAMBAR
25
+ # ===================================================================
26
+
27
+ def analyze_image_with_llms(image_pil):
28
+ """
29
+ Menerima gambar PIL, memanggil 5 LLM secara berurutan, dan mengembalikan hasilnya.
30
+ """
31
+ if image_pil is None:
32
+ return "N/A", "N/A", "N/A", "N/A", "N/A"
33
+
34
+ # Simpan gambar yang diunggah sementara
35
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file:
36
+ # Gunakan mode RGB untuk kompatibilitas yang lebih baik
37
+ image_pil.convert("RGB").save(tmp_file.name, "JPEG", quality=90)
38
+ temp_path = tmp_file.name
39
+
40
+ all_results = {}
41
+
42
+ for model_name in MODEL_NAMES:
43
+ api_key = API_KEYS.get(model_name)
44
+
45
+ if not api_key:
46
+ all_results[model_name] = f"❌ Key Missing"
47
+ continue
48
+
49
+ try:
50
+ # Inisialisasi Detektor
51
+ detector = DeepfakeDetector(
52
+ api_key=api_key,
53
+ model_name=model_name,
54
+ use_face_detector=True # Tetap gunakan cropping RetinaFace
55
+ )
56
+
57
+ # Panggil fungsi deteksi inti
58
+ result, _, _ = detector.detect_deepfake_llm(temp_path)
59
+
60
+ # Ubah output yang ambigu menjadi 'ERROR' untuk tampilan UI yang bersih
61
+ if result == "UNKNOWN" or result == "ERROR":
62
+ all_results[model_name] = f"⚠️ LLM Gagal Tebak"
63
+ else:
64
+ all_results[model_name] = result
65
+
66
+ except Exception as e:
67
+ all_results[model_name] = f"❌ API Error: {str(e)[:50]}"
68
+
69
+ # Tambahkan delay untuk menghindari Rate Limit OpenRouter
70
+ time.sleep(1.5)
71
+
72
+ # Bersihkan file sementara
73
+ os.unlink(temp_path)
74
+
75
+ # Kembalikan hasil dalam urutan yang benar
76
+ return (
77
+ all_results.get("qwen", "Error"),
78
+ all_results.get("gpt", "Error"),
79
+ all_results.get("gemini", "Error"),
80
+ all_results.get("llama", "Error"),
81
+ all_results.get("cohere", "Error"),
82
+ )
83
+
84
+ # ===================================================================
85
+ # 3. INTERFACE GRADIOL
86
+ # ===================================================================
87
+
88
+ iface = gr.Interface(
89
+ fn=analyze_image_with_llms,
90
+ inputs=gr.Image(type="pil", label="🖼️ Upload Wajah untuk Analisis Deepfake"),
91
+ outputs=[
92
+ gr.Textbox(label="1. Qwen Prediction", type="text"),
93
+ gr.Textbox(label="2. GPT-4o Prediction", type="text"),
94
+ gr.Textbox(label="3. Gemini 2.5 Flash Prediction", type="text"),
95
+ gr.Textbox(label="4. Llama 3.2 Vision Prediction", type="text"),
96
+ gr.Textbox(label="5. Cohere Command R+ Prediction", type="text")
97
+ ],
98
+ title="🔬 Perbandingan LLM Multimodal untuk Deteksi Deepfake Wajah",
99
+ description="Unggah gambar wajah. 5 LLM Multimodal (via OpenRouter) akan menganalisis dan menebak: **REAL** atau **FAKE**.",
100
+ allow_flagging="never",
101
+ theme=gr.themes.Soft()
102
+ )
103
+
104
+ if __name__ == "__main__":
105
+ iface.launch()
facecomparison_multi_resume.py ADDED
@@ -0,0 +1,1251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # #!/usr/bin/env python3
2
+ # # import os
3
+ # # import csv
4
+ # # import time
5
+ # # import base64
6
+ # # from pathlib import Path
7
+ # # from tqdm import tqdm
8
+ # # import logging
9
+ # # from PIL import Image
10
+ # # import io
11
+ # # from datetime import datetime
12
+ # # from openai import OpenAI
13
+ # # import numpy as np
14
+
15
+ # # # === RetinaFace Configuration ===
16
+ # # try:
17
+ # # from retinaface import RetinaFace
18
+ # # RETINAFACE_AVAILABLE = True
19
+ # # except ImportError:
20
+ # # RETINAFACE_AVAILABLE = False
21
+ # # print("❌ ERROR: RetinaFace library not found. Please run 'pip install retina-face'. Running without face cropping.")
22
+
23
+ # # # === LOGGING CONFIG ===
24
+ # # os.makedirs("logs", exist_ok=True)
25
+ # # logging.basicConfig(
26
+ # # filename=f"logs/run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log",
27
+ # # level=logging.INFO,
28
+ # # format="%(asctime)s - %(levelname)s - %(message)s",
29
+ # # )
30
+ # # logger = logging.getLogger(__name__)
31
+
32
+
33
+ # # class DeepfakeDetector:
34
+ # # """Deepfake Detection System (Qwen / GPT / Gemini / Llama / Cohere) with RetinaFace + adaptive delay"""
35
+
36
+ # # def __init__(self, api_key, model_name="qwen", debug_mode=False, start_from=0, use_face_detector=True):
37
+ # # self.api_key = api_key
38
+ # # self.model_name = model_name.lower()
39
+ # # self.debug_mode = debug_mode
40
+ # # self.start_from = start_from
41
+ # # self.dataset_folder = "dataset"
42
+ # # self.results_folder = "result"
43
+ # # self.csv_filename = f"result_{self.model_name}_{start_from}.csv"
44
+
45
+ # # # === OpenRouter API client ===
46
+ # # self.client = OpenAI(
47
+ # # base_url="https://openrouter.ai/api/v1",
48
+ # # api_key=self.api_key,
49
+ # # )
50
+ # # self.extra_headers = {
51
+ # # "HTTP-Referer": "https://github.com/retinaface-comparison",
52
+ # # "X-Title": "Deepfake Detection Adaptive"
53
+ # # }
54
+
55
+ # # # === Model map (5 LLMs) ===
56
+ # # self.model_map = {
57
+ # # "qwen": "qwen/qwen3-vl-8b-instruct",
58
+ # # "gpt": "openai/chatgpt-4o-latest",
59
+ # # "gemini": "google/gemini-2.5-flash",
60
+ # # "llama": "meta-llama/llama-3.2-90b-vision-instruct",
61
+ # # "cohere": "cohere/command-r-plus-08-2024",
62
+ # # }
63
+
64
+ # # if self.model_name not in self.model_map:
65
+ # # raise ValueError("❌ Invalid model name. Choose from: qwen, gpt, gemini, llama, cohere")
66
+
67
+ # # logger.info(f"Model selected: {self.model_name.upper()} ({self.model_map[self.model_name]})")
68
+
69
+ # # os.makedirs(self.results_folder, exist_ok=True)
70
+ # # self.use_face_detector = use_face_detector and RETINAFACE_AVAILABLE
71
+ # # self.target_size = 512
72
+
73
+ # # # Adaptive delay system
74
+ # # self.delay = 0.5
75
+ # # self.fail_count = 0
76
+ # # self.success_count = 0
77
+
78
+ # # print(f"\nModel: {self.model_name.upper()} | RetinaFace Cropping: {'ON' if self.use_face_detector else 'OFF'}")
79
+
80
+ # # # === Image handling (RetinaFace) ===
81
+ # # def preprocess_image_with_retinaface(self, image_path):
82
+ # # if not self.use_face_detector or not RETINAFACE_AVAILABLE:
83
+ # # return self.encode_image_simple(image_path)
84
+
85
+ # # try:
86
+ # # faces = RetinaFace.detect_faces(image_path)
87
+ # # if not faces:
88
+ # # logger.warning(f"No face detected in {image_path}")
89
+ # # return self.encode_image_simple(image_path)
90
+
91
+ # # # Ambil wajah utama
92
+ # # first_face = list(faces.values())[0]
93
+ # # facial_area = first_face.get("facial_area", None)
94
+ # # if not facial_area or len(facial_area) != 4:
95
+ # # return self.encode_image_simple(image_path)
96
+
97
+ # # x1, y1, x2, y2 = facial_area
98
+ # # img = Image.open(image_path).convert("RGB")
99
+ # # cropped_img = img.crop((x1, y1, x2, y2))
100
+ # # cropped_img = cropped_img.resize((self.target_size, self.target_size))
101
+
102
+ # # buf = io.BytesIO()
103
+ # # cropped_img.save(buf, format="JPEG", quality=90, optimize=True)
104
+ # # encoded = base64.b64encode(buf.getvalue()).decode("utf-8")
105
+ # # return f"data:image/jpeg;base64,{encoded}"
106
+
107
+ # # except Exception as e:
108
+ # # logger.error(f"RetinaFace error on {image_path}: {e}")
109
+ # # return self.encode_image_simple(image_path)
110
+
111
+ # # def encode_image_simple(self, image_path):
112
+ # # try:
113
+ # # with open(image_path, "rb") as f:
114
+ # # encoded = base64.b64encode(f.read()).decode("utf-8")
115
+ # # return f"data:image/jpeg;base64,{encoded}"
116
+ # # except Exception as e:
117
+ # # logger.error(f"Encode error: {e}")
118
+ # # return None
119
+
120
+ # # def validate_image(self, image_path):
121
+ # # try:
122
+ # # if not os.path.exists(image_path):
123
+ # # return False
124
+ # # with Image.open(image_path) as img:
125
+ # # img.verify()
126
+ # # return True
127
+ # # except Exception:
128
+ # # return False
129
+
130
+ # # def normalize_output(self, content):
131
+ # # if not content:
132
+ # # return "UNKNOWN"
133
+ # # text = content.strip().upper()
134
+ # # if any(w in text for w in ["REAL", "GENUINE", "HUMAN"]):
135
+ # # return "REAL"
136
+ # # if any(w in text for w in ["FAKE", "DEEPFAKE", "AI", "SYNTHETIC", "GENERATED"]):
137
+ # # return "FAKE"
138
+ # # if "NOT FAKE" in text or "LOOKS REAL" in text:
139
+ # # return "REAL"
140
+ # # if "PROBABLY FAKE" in text or "MAYBE FAKE" in text:
141
+ # # return "FAKE"
142
+ # # return "UNKNOWN"
143
+
144
+ # # def reverify_qwen(self, img_b64, prev_result):
145
+ # # prompt = (
146
+ # # "Re-analyze this face image for deepfake signs. "
147
+ # # "Focus on lighting, symmetry, and unnatural skin artifacts. "
148
+ # # "Respond with one word only: REAL or FAKE."
149
+ # # )
150
+ # # print(f"🔁 Re-verifying Qwen result (was {prev_result})...")
151
+ # # try:
152
+ # # resp = self.client.chat.completions.create(
153
+ # # extra_headers=self.extra_headers,
154
+ # # model=self.model_map["qwen"],
155
+ # # messages=[{
156
+ # # "role": "user",
157
+ # # "content": [
158
+ # # {"type": "image_url", "image_url": {"url": img_b64}},
159
+ # # {"type": "text", "text": prompt}
160
+ # # ]
161
+ # # }],
162
+ # # max_tokens=50,
163
+ # # temperature=0.1,
164
+ # # )
165
+ # # content = resp.choices[0].message.content.strip().upper()
166
+ # # if "FAKE" in content:
167
+ # # print("✅ Changed to FAKE after second check")
168
+ # # return "FAKE"
169
+ # # elif "REAL" in content:
170
+ # # print("✅ Confirmed REAL after second check")
171
+ # # return "REAL"
172
+ # # else:
173
+ # # print("⚠️ Still ambiguous after recheck")
174
+ # # return prev_result
175
+ # # except Exception as e:
176
+ # # print(f"⚠️ Qwen re-verification failed: {e}")
177
+ # # return prev_result
178
+
179
+ # # # === Deteksi utama ===
180
+ # # def detect_deepfake_llm(self, image_path):
181
+ # # prompt = (
182
+ # # "You are a forensic image analyst. Analyze this face image for any deepfake or AI manipulation. "
183
+ # # "Consider lighting, eyes, skin, and blending. Respond with only one word: REAL or FAKE."
184
+ # # )
185
+
186
+ # # if not self.validate_image(image_path):
187
+ # # return "ERROR", None, "invalid"
188
+
189
+ # # img_b64 = self.preprocess_image_with_retinaface(image_path)
190
+ # # if not img_b64:
191
+ # # return "ERROR", None, "invalid"
192
+
193
+ # # model_id = self.model_map[self.model_name]
194
+ # # method = "retinaface_crop" if self.use_face_detector else "original"
195
+
196
+ # # try:
197
+ # # resp = self.client.chat.completions.create(
198
+ # # extra_headers=self.extra_headers,
199
+ # # model=model_id,
200
+ # # messages=[{
201
+ # # "role": "user",
202
+ # # "content": [
203
+ # # {"type": "image_url", "image_url": {"url": img_b64}},
204
+ # # {"type": "text", "text": prompt}
205
+ # # ]
206
+ # # }],
207
+ # # max_tokens=50,
208
+ # # temperature=0.1,
209
+ # # )
210
+ # # content = resp.choices[0].message.content
211
+ # # result = self.normalize_output(content)
212
+
213
+ # # if self.model_name == "qwen" and result == "REAL":
214
+ # # result = self.reverify_qwen(img_b64, result)
215
+
216
+ # # return result, content, method
217
+
218
+ # # except Exception as e:
219
+ # # logger.error(f"Detection failed: {e}")
220
+ # # return "ERROR", None, method
221
+
222
+ # # # === Dataset & Resume ===
223
+ # # def get_images(self):
224
+ # # dataset_path = Path(self.dataset_folder)
225
+ # # real_path = dataset_path / "face_real"
226
+ # # fake_path = dataset_path / "face_fake"
227
+ # # if not real_path.exists() or not fake_path.exists():
228
+ # # print("❌ Dataset folders missing.")
229
+ # # return []
230
+ # # real_images = sorted(list(real_path.glob("*.jpg")))[:500]
231
+ # # fake_images = sorted(list(fake_path.glob("*.jpg")))[:500]
232
+ # # return [(str(p), "REAL") for p in real_images] + [(str(p), "FAKE") for p in fake_images]
233
+
234
+ # # def load_existing_results(self):
235
+ # # csv_path = os.path.join(self.results_folder, self.csv_filename)
236
+ # # if not os.path.exists(csv_path):
237
+ # # return []
238
+ # # results = []
239
+ # # with open(csv_path, "r", encoding="utf-8") as f:
240
+ # # reader = csv.reader(f)
241
+ # # next(reader)
242
+ # # for row in reader:
243
+ # # if len(row) >= 5:
244
+ # # results.append((row[0], row[1], row[2], row[3], row[4]))
245
+ # # logger.info(f"Loaded {len(results)} existing results")
246
+ # # return results
247
+
248
+ # # def save_results_to_csv(self, results):
249
+ # # csv_path = os.path.join(self.results_folder, self.csv_filename)
250
+ # # with open(csv_path, "w", newline="", encoding="utf-8") as f:
251
+ # # writer = csv.writer(f)
252
+ # # writer.writerow(["filename", "ground_truth", "llm_result", "model_name", "method"])
253
+ # # writer.writerows(results)
254
+ # # logger.info(f"Saved {len(results)} results to {csv_path}")
255
+
256
+ # # # === Adaptive delay logic ===
257
+ # # def adjust_delay(self):
258
+ # # if self.fail_count > 5:
259
+ # # self.delay = min(self.delay + 0.2, 2.0)
260
+ # # logger.warning(f"Increasing delay to {self.delay:.1f}s due to failures.")
261
+ # # self.fail_count = 0
262
+ # # elif self.success_count > 10:
263
+ # # self.delay = max(self.delay - 0.1, 0.3)
264
+ # # logger.info(f"Reducing delay to {self.delay:.1f}s (stable).")
265
+ # # self.success_count = 0
266
+
267
+ # # def run_detection(self, resume=True):
268
+ # # all_images = self.get_images()
269
+ # # if not all_images:
270
+ # # return
271
+
272
+ # # results = self.load_existing_results() if resume else []
273
+ # # processed = {r[0] for r in results}
274
+ # # remaining = [(p, gt) for p, gt in all_images if os.path.basename(p) not in processed]
275
+
276
+ # # print(f"\n=== STARTING {self.model_name.upper()} DETECTION ===")
277
+ # # print(f"Total: {len(all_images)} | Already done: {len(processed)} | Remaining: {len(remaining)}")
278
+
279
+ # # with tqdm(total=len(remaining), desc=f"{self.model_name.upper()}") as pbar:
280
+ # # for img_path, truth in remaining:
281
+ # # try:
282
+ # # result, response, method = self.detect_deepfake_llm(img_path)
283
+ # # results.append((os.path.basename(img_path), truth, result, self.model_name, method))
284
+ # # self.success_count += 1
285
+ # # except Exception as e:
286
+ # # logger.error(f"Fatal error: {e}")
287
+ # # results.append((os.path.basename(img_path), truth, "ERROR", self.model_name, "error"))
288
+ # # self.fail_count += 1
289
+
290
+ # # pbar.set_description(f"{os.path.basename(img_path)} -> {result}")
291
+ # # pbar.update(1)
292
+ # # self.save_results_to_csv(results)
293
+ # # self.adjust_delay()
294
+ # # time.sleep(self.delay)
295
+
296
+ # # print(f"\n✅ Detection completed for {self.model_name.upper()}")
297
+ # # print(f"Results saved to: {os.path.join(self.results_folder, self.csv_filename)}")
298
+
299
+ # #!/usr/bin/env python3
300
+ # #!/usr/bin/env python3
301
+ # import os
302
+ # import csv
303
+ # import time
304
+ # import base64
305
+ # from pathlib import Path
306
+ # from tqdm import tqdm
307
+ # import logging
308
+ # from PIL import Image
309
+ # import io
310
+ # from datetime import datetime
311
+ # from openai import OpenAI
312
+ # import numpy as np
313
+ # import math # Diperlukan untuk perhitungan akurasi
314
+
315
+ # # RetinaFace Configuration
316
+ # try:
317
+ # #retinaface
318
+ # from retinaface import RetinaFace
319
+ # RETINAFACE_AVAILABLE = True
320
+ # except ImportError:
321
+ # RETINAFACE_AVAILABLE = False
322
+ # print("❌ ERROR: RetinaFace library not found. Running without face cropping.")
323
+
324
+ # # === LOGGING CONFIG ===
325
+ # os.makedirs("logs", exist_ok=True)
326
+ # logging.basicConfig(
327
+ # filename=f"logs/run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log",
328
+ # level=logging.INFO,
329
+ # format="%(asctime)s - %(levelname)s - %(message)s",
330
+ # )
331
+ # logger = logging.getLogger(__name__)
332
+
333
+
334
+ # class DeepfakeDetector:
335
+ # """Deepfake Detection System (Qwen / GPT / Gemini / Llama / Cohere) with RetinaFace + adaptive delay"""
336
+
337
+ # def __init__(self, api_key, model_name="qwen", debug_mode=False, start_from=0, use_face_detector=True):
338
+ # self.api_key = api_key
339
+ # self.model_name = model_name.lower()
340
+ # self.debug_mode = debug_mode
341
+ # self.start_from = start_from
342
+ # self.dataset_folder = "dataset"
343
+ # self.results_folder = "result"
344
+ # self.csv_filename = f"result_{self.model_name}_{start_from}.csv"
345
+
346
+ # # OpenRouter API client
347
+ # self.client = OpenAI(
348
+ # base_url="https://openrouter.ai/api/v1",
349
+ # api_key=self.api_key,
350
+ # )
351
+ # self.extra_headers = {
352
+ # "HTTP-Referer": "https://github.com/retinaface-comparison",
353
+ # "X-Title": "Deepfake Detection Adaptive"
354
+ # }
355
+
356
+ # # Model map (5 LLMs via OpenRouter)
357
+ # self.model_map = {
358
+ # "qwen": "qwen/qwen3-vl-8b-instruct",
359
+ # "gpt": "openai/chatgpt-4o-latest",
360
+ # "gemini": "google/gemini-2.5-flash",
361
+ # "llama": "meta-llama/llama-3.2-90b-vision-instruct",
362
+ # "cohere": "cohere/command-r-plus-08-2024",
363
+ # }
364
+
365
+ # if self.model_name not in self.model_map:
366
+ # raise ValueError("❌ Invalid model name. Choose from: qwen, gpt, gemini, llama, cohere")
367
+
368
+ # logger.info(f"Model selected: {self.model_name.upper()} ({self.model_map[self.model_name]})")
369
+
370
+ # os.makedirs(self.results_folder, exist_ok=True)
371
+ # self.use_face_detector = use_face_detector and RETINAFACE_AVAILABLE
372
+ # self.target_size = 512
373
+
374
+ # # waktu delay
375
+ # self.delay = 0.3
376
+ # self.fail_count = 0
377
+ # self.success_count = 0
378
+
379
+ # print(f"\nModel: {self.model_name.upper()} | RetinaFace Cropping: {'ON' if self.use_face_detector else 'OFF'}")
380
+
381
+ # # Image handling (RetinaFace)
382
+ # def preprocess_image_with_retinaface(self, image_path):
383
+ # if not self.use_face_detector or not RETINAFACE_AVAILABLE:
384
+ # return self.encode_image_simple(image_path)
385
+
386
+ # try:
387
+ # # Perlu diubah ke string karena RetinaFace kadang tidak menerima objek Path
388
+ # faces = RetinaFace.detect_faces(str(image_path))
389
+ # if not faces:
390
+ # logger.warning(f"No face detected in {image_path}")
391
+ # return self.encode_image_simple(image_path)
392
+
393
+ # first_face = list(faces.values())[0]
394
+ # facial_area = first_face.get("facial_area", None)
395
+ # if not facial_area or len(facial_area) != 4:
396
+ # return self.encode_image_simple(image_path)
397
+
398
+ # x1, y1, x2, y2 = facial_area
399
+ # img = Image.open(image_path).convert("RGB")
400
+
401
+ # # Tambahkan margin (ekstraksi)
402
+ # margin_ratio = 0.2
403
+ # w, h = x2 - x1, y2 - y1
404
+ # margin_x = int(w * margin_ratio)
405
+ # margin_y = int(h * margin_ratio)
406
+
407
+ # x1 = max(0, x1 - margin_x)
408
+ # y1 = max(0, y1 - margin_y)
409
+ # x2 = min(img.width, x2 + margin_x)
410
+ # y2 = min(img.height, y2 + margin_y)
411
+
412
+ # cropped_img = img.crop((x1, y1, x2, y2))
413
+ # cropped_img = cropped_img.resize((self.target_size, self.target_size), Image.Resampling.LANCZOS)
414
+
415
+ # buf = io.BytesIO()
416
+ # cropped_img.save(buf, format="JPEG", quality=90, optimize=True)
417
+ # encoded = base64.b64encode(buf.getvalue()).decode("utf-8")
418
+ # return f"data:image/jpeg;base64,{encoded}"
419
+
420
+ # except Exception as e:
421
+ # logger.error(f"RetinaFace error on {image_path}: {e}")
422
+ # return self.encode_image_simple(image_path)
423
+
424
+ # def encode_image_simple(self, image_path):
425
+ # try:
426
+ # with open(image_path, "rb") as f:
427
+ # encoded = base64.b64encode(f.read()).decode("utf-8")
428
+ # return f"data:image/jpeg;base64,{encoded}"
429
+ # except Exception as e:
430
+ # logger.error(f"Encode error: {e}")
431
+ # return None
432
+
433
+ # def validate_image(self, image_path):
434
+ # try:
435
+ # if not os.path.exists(image_path):
436
+ # return False
437
+ # with Image.open(image_path) as img:
438
+ # img.verify()
439
+ # return True
440
+ # except Exception:
441
+ # return False
442
+
443
+ # def normalize_output(self, content):
444
+ # """
445
+ # Normalizes verbose LLM output to a single word: REAL, FAKE, or UNKNOWN.
446
+ # """
447
+ # if not content:
448
+ # return "UNKNOWN"
449
+
450
+ # text = content.strip().upper()
451
+
452
+ # # Mencari kata kunci FAKE
453
+ # if any(w in text for w in ["FAKE", "DEEPFAKE", "AI GENERATED", "SYNTHETIC"]):
454
+ # return "FAKE"
455
+
456
+ # # Mencari kata kunci REAL
457
+ # if any(w in text for w in ["REAL", "GENUINE", "HUMAN", "NOT FAKE"]):
458
+ # return "REAL"
459
+
460
+ # # Upaya kedua: Coba ambil kata pertama/kata kunci di tengah respons
461
+ # words = text.split()
462
+ # if words:
463
+ # for word in words[:3]: # Cek 3 kata pertama
464
+ # if "REAL" in word: return "REAL"
465
+ # if "FAKE" in word: return "FAKE"
466
+
467
+ # logger.warning(f"Output ambiguous/unpredictable: {content}")
468
+ # return "UNKNOWN"
469
+
470
+ # def reverify_qwen(self, img_b64, prev_result):
471
+ # # Logika re-verifikasi
472
+ # prompt = (
473
+ # "Re-analyze this face image for deepfake signs. "
474
+ # "Focus on lighting, symmetry, and unnatural skin artifacts. "
475
+ # "Respond with one word only: REAL or FAKE."
476
+ # )
477
+ # print(f"🔁 Re-verifying Qwen result (was {prev_result})...")
478
+ # try:
479
+ # resp = self.client.chat.completions.create(
480
+ # extra_headers=self.extra_headers,
481
+ # model=self.model_map["qwen"],
482
+ # messages=[{
483
+ # "role": "user",
484
+ # "content": [
485
+ # {"type": "image_url", "image_url": {"url": img_b64}},
486
+ # {"type": "text", "text": prompt}
487
+ # ]
488
+ # }],
489
+ # max_tokens=50,
490
+ # temperature=0.1,
491
+ # )
492
+ # content = resp.choices[0].message.content.strip().upper()
493
+ # if "FAKE" in content:
494
+ # print("Changed to FAKE after second check")
495
+ # return "FAKE"
496
+ # elif "REAL" in content:
497
+ # print("Confirmed REAL after second check")
498
+ # return "REAL"
499
+ # else:
500
+ # print("Still ambiguous after recheck")
501
+ # return prev_result
502
+ # except Exception as e:
503
+ # print(f"Qwen re-verification failed: {e}")
504
+ # return prev_result
505
+
506
+ # # Fungsi Fallback
507
+ # def retinaface_simple_fallback(self, image_path):
508
+ # """Applies a heuristic rule if LLM returns an error."""
509
+ # if not self.use_face_detector or not RETINAFACE_AVAILABLE:
510
+ # return 'UNKNOWN_FALLBACK'
511
+
512
+ # try:
513
+ # # Panggil deteksi wajah (menggunakan str(image_path))
514
+ # faces = RetinaFace.detect_faces(str(image_path))
515
+ # if not faces:
516
+ # return 'UNKNOWN_FALLBACK'
517
+
518
+ # best_score = max(f['score'] for f in faces.values())
519
+
520
+ # # Aturan Heuristik: Jika skor kepercayaan wajah sangat tinggi, REAL.
521
+ # if best_score > 0.995:
522
+ # return 'REAL'
523
+ # else:
524
+ # return 'UNKNOWN_FALLBACK'
525
+
526
+ # except Exception:
527
+ # return 'UNKNOWN_FALLBACK'
528
+
529
+ # # Deteksi utama
530
+ # def detect_deepfake_llm(self, image_path):
531
+ # prompt = (
532
+ # "You are a forensic image analyst. Analyze this face image for any deepfake or AI manipulation. "
533
+ # "Consider lighting, eyes, skin, and blending. Respond with only one word: REAL or FAKE."
534
+ # )
535
+
536
+ # if not self.validate_image(image_path):
537
+ # return "ERROR", None, "invalid"
538
+
539
+ # img_b64 = self.preprocess_image_with_retinaface(image_path)
540
+ # if not img_b64:
541
+ # return "ERROR", None, "invalid"
542
+
543
+ # model_id = self.model_map[self.model_name]
544
+ # method = "retinaface_crop" if self.use_face_detector else "original"
545
+
546
+ # try:
547
+ # resp = self.client.chat.completions.create(
548
+ # extra_headers=self.extra_headers,
549
+ # model=model_id,
550
+ # messages=[{
551
+ # "role": "user",
552
+ # "content": [
553
+ # {"type": "image_url", "image_url": {"url": img_b64}},
554
+ # {"type": "text", "text": prompt}
555
+ # ]
556
+ # }],
557
+ # max_tokens=50,
558
+ # temperature=0.1,
559
+ # )
560
+ # content = resp.choices[0].message.content
561
+ # result = self.normalize_output(content)
562
+
563
+ # if self.model_name == "qwen" and result == "REAL":
564
+ # result = self.reverify_qwen(img_b64, result)
565
+
566
+ # return result, content, method
567
+
568
+ # except Exception as e:
569
+ # logger.error(f"Detection failed: {e}")
570
+
571
+ # # Tambahkan logika Fallback RetinaFace
572
+ # fallback_result = self.retinaface_simple_fallback(image_path)
573
+ # logger.warning(f"LLM Failed. Applying Fallback Logic: {fallback_result}")
574
+
575
+ # if fallback_result == 'REAL':
576
+ # # Jika heuristik RetinaFace yakin gambar BERKUALITAS BAGUS, prediksi REAL
577
+ # return 'REAL', "RetinaFace Heuristic", "retinaface_crop_FALLBACK"
578
+
579
+ # else:
580
+ # return "FAKE", "RetinaFace Heuristic (Assumed FAKE)", "retinaface_crop"
581
+
582
+ # # === Dataset & Resume ===
583
+ # def get_images(self):
584
+ # dataset_path = Path(self.dataset_folder)
585
+ # real_path = dataset_path / "face_real"
586
+ # fake_path = dataset_path / "face_fake"
587
+ # if not real_path.exists() or not fake_path.exists():
588
+ # print("❌ Dataset folders missing.")
589
+ # return []
590
+
591
+ # # Batasi gambar menjadi 500 REAL dan 500 FAKE (total 1000)
592
+ # real_images = sorted(list(real_path.glob("*.jpg")))[:500]
593
+ # fake_images = sorted(list(fake_path.glob("*.jpg")))[:500]
594
+
595
+ # return [(str(p), "REAL") for p in real_images] + [(str(p), "FAKE") for p in fake_images]
596
+
597
+ # def load_existing_results(self):
598
+ # csv_path = os.path.join(self.results_folder, self.csv_filename)
599
+ # if not os.path.exists(csv_path):
600
+ # return []
601
+ # results = []
602
+ # with open(csv_path, "r", encoding="utf-8") as f:
603
+ # reader = csv.reader(f)
604
+ # next(reader)
605
+ # for row in reader:
606
+ # # baris memiliki 5 kolom
607
+ # if len(row) >= 5:
608
+ # results.append((row[0], row[1], row[2], row[3], row[4]))
609
+ # logger.info(f"Loaded {len(results)} existing results")
610
+ # return results
611
+
612
+ # def save_results_to_csv(self, results):
613
+ # csv_path = os.path.join(self.results_folder, self.csv_filename)
614
+ # with open(csv_path, "w", newline="", encoding="utf-8") as f:
615
+ # writer = csv.writer(f)
616
+ # writer.writerow(["filename", "ground_truth", "llm_result", "model_name", "method"])
617
+ # writer.writerows(results)
618
+ # logger.info(f"Saved {len(results)} results to {csv_path}")
619
+
620
+ # # logika delay
621
+ # def adjust_delay(self):
622
+ # # Logika adaptive delay
623
+ # if self.fail_count > 5:
624
+ # self.delay = min(self.delay + 0.2, 2.0)
625
+ # logger.warning(f"Increasing delay to {self.delay:.1f}s due to failures.")
626
+ # self.fail_count = 0
627
+ # elif self.success_count > 10:
628
+ # self.delay = max(self.delay - 0.1, 0.3)
629
+ # logger.info(f"Reducing delay to {self.delay:.1f}s (stable).")
630
+ # self.success_count = 0
631
+
632
+ # def run_detection(self, resume=True):
633
+ # all_images = self.get_images()
634
+ # if not all_images:
635
+ # return
636
+
637
+ # results = self.load_existing_results() if resume else []
638
+ # processed = {r[0] for r in results}
639
+ # remaining = [(p, gt) for p, gt in all_images if os.path.basename(p) not in processed]
640
+
641
+ # print(f"\n=== STARTING {self.model_name.upper()} DETECTION ===")
642
+ # print(f"Total: {len(all_images)} | Already done: {len(processed)} | Remaining: {len(remaining)}")
643
+
644
+ # with tqdm(total=len(remaining), desc=f"{self.model_name.upper()}") as pbar:
645
+ # for img_path, truth in remaining:
646
+ # try:
647
+ # result, response, method = self.detect_deepfake_llm(img_path)
648
+ # results.append((os.path.basename(img_path), truth, result, self.model_name, method))
649
+ # self.success_count += 1
650
+ # except Exception as e:
651
+ # logger.error(f"Fatal error: {e}")
652
+ # results.append((os.path.basename(img_path), truth, "ERROR", self.model_name, "error"))
653
+ # self.fail_count += 1
654
+
655
+ # pbar.set_description(f"{os.path.basename(img_path)} -> {result}")
656
+ # pbar.update(1)
657
+ # self.save_results_to_csv(results)
658
+ # self.adjust_delay()
659
+ # time.sleep(self.delay)
660
+
661
+ # print(f"\n✅ Deteksi selesai untuk {self.model_name.upper()}")
662
+ # print(f"Hasil simpan ke: {os.path.join(self.results_folder, self.csv_filename)}")
663
+
664
+ # #!/usr/bin/env python3
665
+ # import os
666
+ # import csv
667
+ # import time
668
+ # import base64
669
+ # from pathlib import Path
670
+ # from tqdm import tqdm
671
+ # import logging
672
+ # from PIL import Image
673
+ # import io
674
+ # from datetime import datetime
675
+ # from openai import OpenAI
676
+ # import numpy as np
677
+ # import math
678
+
679
+ # # === RetinaFace Configuration ===
680
+ # try:
681
+ # from retinaface import RetinaFace
682
+ # RETINAFACE_AVAILABLE = True
683
+ # except ImportError:
684
+ # RETINAFACE_AVAILABLE = False
685
+ # print("❌ ERROR: RetinaFace library not found. Please run 'pip install retina-face'. Running without face cropping.")
686
+
687
+ # # === LOGGING CONFIG ===
688
+ # os.makedirs("logs", exist_ok=True)
689
+ # logging.basicConfig(
690
+ # filename=f"logs/run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log",
691
+ # level=logging.INFO,
692
+ # format="%(asctime)s - %(levelname)s - %(message)s",
693
+ # )
694
+ # logger = logging.getLogger(__name__)
695
+
696
+
697
+ # class DeepfakeDetector:
698
+ # """Deepfake Detection System (Qwen / GPT / Gemini / Llama / Cohere) with RetinaFace + adaptive delay"""
699
+
700
+ # def __init__(self, api_key, model_name="qwen", debug_mode=False, start_from=0, use_face_detector=True):
701
+ # self.api_key = api_key
702
+ # self.model_name = model_name.lower()
703
+ # self.debug_mode = debug_mode
704
+ # self.start_from = start_from
705
+ # self.dataset_folder = "dataset"
706
+ # self.results_folder = "result"
707
+ # self.csv_filename = f"result_{self.model_name}_{start_from}.csv"
708
+
709
+ # # === OpenRouter API client (Menggunakan OpenRouter untuk semua model) ===
710
+ # self.client = OpenAI(
711
+ # base_url="https://openrouter.ai/api/v1",
712
+ # api_key=self.api_key,
713
+ # )
714
+ # self.extra_headers = {
715
+ # "HTTP-Referer": "https://github.com/retinaface-comparison",
716
+ # "X-Title": "Deepfake Detection Adaptive"
717
+ # }
718
+
719
+ # # === Model map (5 LLMs via OpenRouter) ===
720
+ # self.model_map = {
721
+ # "qwen": "qwen/qwen3-vl-8b-instruct",
722
+ # "gpt": "openai/chatgpt-4o-latest",
723
+ # "gemini": "google/gemini-2.5-flash",
724
+ # "llama": "meta-llama/llama-3.2-90b-vision-instruct",
725
+ # "cohere": "cohere/command-r-plus-08-2024",
726
+ # }
727
+
728
+ # if self.model_name not in self.model_map:
729
+ # raise ValueError("❌ Invalid model name. Choose from: qwen, gpt, gemini, llama, cohere")
730
+
731
+ # logger.info(f"Model selected: {self.model_name.upper()} ({self.model_map[self.model_name]})")
732
+
733
+ # os.makedirs(self.results_folder, exist_ok=True)
734
+ # self.use_face_detector = False and RETINAFACE_AVAILABLE
735
+ # self.target_size = 512
736
+
737
+ # # waktu delay
738
+ # self.delay = 0.3
739
+ # self.fail_count = 0
740
+ # self.success_count = 0
741
+
742
+ # print(f"\nModel: {self.model_name.upper()} | RetinaFace Cropping: {'ON' if self.use_face_detector else 'OFF'}")
743
+
744
+ # # === Image handling (RetinaFace) ===
745
+ # def preprocess_image_with_retinaface(self, image_path):
746
+ # if not self.use_face_detector or not RETINAFACE_AVAILABLE:
747
+ # return self.encode_image_simple(image_path)
748
+
749
+ # try:
750
+ # # Perlu diubah ke string karena RetinaFace kadang tidak menerima objek Path
751
+ # faces = RetinaFace.detect_faces(str(image_path))
752
+ # if not faces:
753
+ # logger.warning(f"No face detected in {image_path}")
754
+ # return self.encode_image_simple(image_path)
755
+
756
+ # first_face = list(faces.values())[0]
757
+ # facial_area = first_face.get("facial_area", None)
758
+ # if not facial_area or len(facial_area) != 4:
759
+ # return self.encode_image_simple(image_path)
760
+
761
+ # x1, y1, x2, y2 = facial_area
762
+ # img = Image.open(image_path).convert("RGB")
763
+
764
+ # # Tambahkan margin (ekstraksi)
765
+ # margin_ratio = 0.2
766
+ # w, h = x2 - x1, y2 - y1
767
+ # margin_x = int(w * margin_ratio)
768
+ # margin_y = int(h * margin_ratio)
769
+
770
+ # x1 = max(0, x1 - margin_x)
771
+ # y1 = max(0, y1 - margin_y)
772
+ # x2 = min(img.width, x2 + margin_x)
773
+ # y2 = min(img.height, y2 + margin_y)
774
+
775
+ # cropped_img = img.crop((x1, y1, x2, y2))
776
+ # cropped_img = cropped_img.resize((self.target_size, self.target_size), Image.Resampling.LANCZOS)
777
+
778
+ # buf = io.BytesIO()
779
+ # cropped_img.save(buf, format="JPEG", quality=90, optimize=True)
780
+ # encoded = base64.b64encode(buf.getvalue()).decode("utf-8")
781
+ # return f"data:image/jpeg;base64,{encoded}"
782
+
783
+ # except Exception as e:
784
+ # logger.error(f"RetinaFace error on {image_path}: {e}")
785
+ # return self.encode_image_simple(image_path)
786
+
787
+ # def encode_image_simple(self, image_path):
788
+ # try:
789
+ # with open(image_path, "rb") as f:
790
+ # encoded = base64.b64encode(f.read()).decode("utf-8")
791
+ # return f"data:image/jpeg;base64,{encoded}"
792
+ # except Exception as e:
793
+ # logger.error(f"Encode error: {e}")
794
+ # return None
795
+
796
+ # def validate_image(self, image_path):
797
+ # try:
798
+ # if not os.path.exists(image_path):
799
+ # return False
800
+ # with Image.open(image_path) as img:
801
+ # img.verify()
802
+ # return True
803
+ # except Exception:
804
+ # return False
805
+
806
+ # def normalize_output(self, content):
807
+ # """
808
+ # Normalizes verbose LLM output to a single word: REAL, FAKE, or UNKNOWN.
809
+ # """
810
+ # if not content:
811
+ # return "UNKNOWN"
812
+
813
+ # text = content.strip().upper()
814
+
815
+ # # Mencari kata kunci FAKE (atau sinonim)
816
+ # if any(w in text for w in ["FAKE", "DEEPFAKE", "AI GENERATED", "SYNTHETIC"]):
817
+ # return "FAKE"
818
+
819
+ # # Mencari kata kunci REAL (atau sinonim)
820
+
821
+ # if any(w in text for w in ["REAL", "GENUINE", "HUMAN", "NOT FAKE"]):
822
+ # return "REAL"
823
+
824
+ # # Upaya kedua: Coba ambil kata pertama/kata kunci di tengah respons
825
+ # words = text.split()
826
+ # if words:
827
+ # for word in words[:3]: # Cek 3 kata pertama
828
+ # if "REAL" in word: return "REAL"
829
+ # if "FAKE" in word: return "FAKE"
830
+
831
+ # logger.warning(f"Output ambiguous/unpredictable: {content}")
832
+ # return "UNKNOWN"
833
+
834
+ # def reverify_qwen(self, img_b64, prev_result):
835
+ # # Logika re-verifikasi
836
+ # prompt = (
837
+ # "Re-analyze this face image for deepfake signs. "
838
+ # "Focus on lighting, symmetry, and unnatural skin artifacts. "
839
+ # "Respond with one word only: REAL or FAKE."
840
+ # )
841
+ # print(f"🔁 Re-verifying Qwen result (was {prev_result})...")
842
+ # try:
843
+ # resp = self.client.chat.completions.create(
844
+ # extra_headers=self.extra_headers,
845
+ # model=self.model_map["qwen"],
846
+ # messages=[{
847
+ # "role": "user",
848
+ # "content": [
849
+ # {"type": "image_url", "image_url": {"url": img_b64}},
850
+ # {"type": "text", "text": prompt}
851
+ # ]
852
+ # }],
853
+ # max_tokens=50,
854
+ # temperature=0.1,
855
+ # )
856
+ # content = resp.choices[0].message.content.strip().upper()
857
+ # if "FAKE" in content:
858
+ # print("Changed to FAKE after second check")
859
+ # return "FAKE"
860
+ # elif "REAL" in content:
861
+ # print("Confirmed REAL after second check")
862
+ # return "REAL"
863
+ # else:
864
+ # print("Still ambiguous after recheck")
865
+ # return prev_result
866
+ # except Exception as e:
867
+ # print(f"Qwen re-verification failed: {e}")
868
+ # return prev_result
869
+
870
+ # # Fungsi Fallback Sederhana RetinaFace (Heuristik) DIHAPUS
871
+
872
+ # # === Deteksi utama ===
873
+ # def detect_deepfake_llm(self, image_path):
874
+ # prompt = (
875
+ # "You are a forensic image analyst. Analyze this face image for any deepfake or AI manipulation. "
876
+ # "Consider lighting, eyes, skin, and blending. Respond with only one word: REAL or FAKE."
877
+ # )
878
+
879
+ # if not self.validate_image(image_path):
880
+ # return "ERROR", None, "invalid"
881
+
882
+ # img_b64 = self.preprocess_image_with_retinaface(image_path)
883
+ # if not img_b64:
884
+ # return "ERROR", None, "invalid"
885
+
886
+ # model_id = self.model_map[self.model_name]
887
+ # method = "retinaface_crop" if self.use_face_detector else "original"
888
+
889
+ # try:
890
+ # resp = self.client.chat.completions.create(
891
+ # extra_headers=self.extra_headers,
892
+ # model=model_id,
893
+ # messages=[{
894
+ # "role": "user",
895
+ # "content": [
896
+ # {"type": "image_url", "image_url": {"url": img_b64}},
897
+ # {"type": "text", "text": prompt}
898
+ # ]
899
+ # }],
900
+ # max_tokens=50,
901
+ # temperature=0.1,
902
+ # )
903
+ # content = resp.choices[0].message.content
904
+ # result = self.normalize_output(content)
905
+
906
+ # if self.model_name == "qwen" and result == "REAL":
907
+ # result = self.reverify_qwen(img_b64, result)
908
+
909
+ # return result, content, method
910
+
911
+ # except Exception as e:
912
+ # logger.error(f"Detection failed: {e}")
913
+
914
+ # # --- LOGIKA KETIKA LLM GAGAL (TIDAK ADA TEBAKAN) ---
915
+ # # Jika LLM gagal, catat sebagai ERROR.
916
+ # return "ERROR", None, "API_FAILURE" # Mengganti 'error' dengan 'API_FAILURE' untuk kejelasan
917
+
918
+ # # === Dataset & Resume ===
919
+ # def get_images(self):
920
+ # dataset_path = Path(self.dataset_folder)
921
+ # real_path = dataset_path / "face_real"
922
+ # fake_path = dataset_path / "face_fake"
923
+ # if not real_path.exists() or not fake_path.exists():
924
+ # print("❌ Dataset folders missing.")
925
+ # return []
926
+
927
+ # # Batasi gambar menjadi 500 REAL dan 500 FAKE (total 1000)
928
+ # real_images = sorted(list(real_path.glob("*.jpg")))[:500]
929
+ # fake_images = sorted(list(fake_path.glob("*.jpg")))[:500]
930
+
931
+ # return [(str(p), "REAL") for p in real_images] + [(str(p), "FAKE") for p in fake_images]
932
+
933
+ # def load_existing_results(self):
934
+ # csv_path = os.path.join(self.results_folder, self.csv_filename)
935
+ # if not os.path.exists(csv_path):
936
+ # return []
937
+ # results = []
938
+ # with open(csv_path, "r", encoding="utf-8") as f:
939
+ # reader = csv.reader(f)
940
+ # next(reader)
941
+ # for row in reader:
942
+ # # Pastikan baris memiliki 5 kolom
943
+ # if len(row) >= 5:
944
+ # results.append((row[0], row[1], row[2], row[3], row[4]))
945
+ # logger.info(f"Loaded {len(results)} existing results")
946
+ # return results
947
+
948
+ # def save_results_to_csv(self, results):
949
+ # csv_path = os.path.join(self.results_folder, self.csv_filename)
950
+ # with open(csv_path, "w", newline="", encoding="utf-8") as f:
951
+ # writer = csv.writer(f)
952
+ # writer.writerow(["filename", "ground_truth", "llm_result", "model_name", "method"])
953
+ # writer.writerows(results)
954
+ # logger.info(f"Saved {len(results)} results to {csv_path}")
955
+
956
+ # # === Adaptive delay logic ===
957
+ # def adjust_delay(self):
958
+ # # Logika adaptive delay
959
+ # if self.fail_count > 5:
960
+ # self.delay = min(self.delay + 0.2, 2.0)
961
+ # logger.warning(f"Increasing delay to {self.delay:.1f}s due to failures.")
962
+ # self.fail_count = 0
963
+ # elif self.success_count > 10:
964
+ # self.delay = max(self.delay - 0.1, 0.3)
965
+ # logger.info(f"Reducing delay to {self.delay:.1f}s (stable).")
966
+ # self.success_count = 0
967
+
968
+ # def run_detection(self, resume=True):
969
+ # all_images = self.get_images()
970
+ # if not all_images:
971
+ # return
972
+
973
+ # results = self.load_existing_results() if resume else []
974
+ # processed = {r[0] for r in results}
975
+ # remaining = [(p, gt) for p, gt in all_images if os.path.basename(p) not in processed]
976
+
977
+ # print(f"\n=== STARTING {self.model_name.upper()} DETECTION ===")
978
+ # print(f"Total: {len(all_images)} | Already done: {len(processed)} | Remaining: {len(remaining)}")
979
+
980
+ # with tqdm(total=len(remaining), desc=f"{self.model_name.upper()}") as pbar:
981
+ # for img_path, truth in remaining:
982
+ # try:
983
+ # result, response, method = self.detect_deepfake_llm(img_path)
984
+ # results.append((os.path.basename(img_path), truth, result, self.model_name, method))
985
+ # self.success_count += 1
986
+ # except Exception as e:
987
+ # logger.error(f"Fatal error: {e}")
988
+ # results.append((os.path.basename(img_path), truth, "ERROR", self.model_name, "error"))
989
+ # self.fail_count += 1
990
+
991
+ # pbar.set_description(f"{os.path.basename(img_path)} -> {result}")
992
+ # pbar.update(1)
993
+ # self.save_results_to_csv(results)
994
+ # self.adjust_delay()
995
+ # time.sleep(self.delay)
996
+
997
+ # print(f"\n✅ Deteksi selesai untuk {self.model_name.upper()}")
998
+ # print(f"Hasil simpan ke: {os.path.join(self.results_folder, self.csv_filename)}")
999
+
1000
+ import os
1001
+ import time
1002
+ import base64
1003
+ from pathlib import Path
1004
+ import logging
1005
+ from PIL import Image
1006
+ import io
1007
+ from datetime import datetime
1008
+ from openai import OpenAI
1009
+ import numpy as np
1010
+ # Hapus: import csv (Tidak diperlukan untuk inference Gradio)
1011
+ # Hapus: import tqdm (Tidak diperlukan untuk inference Gradio)
1012
+ # Hapus: import math (Tidak diperlukan untuk inference Gradio)
1013
+
1014
+ # === RetinaFace Configuration ===
1015
+ try:
1016
+ from retinaface import RetinaFace
1017
+ RETINAFACE_AVAILABLE = True
1018
+ except ImportError:
1019
+ RETINAFACE_AVAILABLE = False
1020
+ # Di lingkungan Gradio, pesan ini biasanya tidak terlihat, tapi biarkan saja.
1021
+ # print("❌ ERROR: RetinaFace library not found. Running without face cropping.")
1022
+
1023
+ # === LOGGING CONFIG ===
1024
+ os.makedirs("logs", exist_ok=True)
1025
+ logging.basicConfig(
1026
+ filename=f"logs/run_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log",
1027
+ level=logging.INFO,
1028
+ format="%(asctime)s - %(levelname)s - %(message)s",
1029
+ )
1030
+ logger = logging.getLogger(__name__)
1031
+
1032
+
1033
+ class DeepfakeDetector:
1034
+ """Deepfake Detection System (Qwen / GPT / Gemini / Llama / Cohere) for Single Image Inference"""
1035
+
1036
+ # Hapus: start_from=0, karena tidak relevan untuk single inference
1037
+ def __init__(self, api_key, model_name="qwen", use_face_detector=True):
1038
+ self.api_key = api_key
1039
+ self.model_name = model_name.lower()
1040
+ # self.debug_mode = debug_mode # Dihapus, karena tidak digunakan
1041
+
1042
+ # Hapus properti terkait resume/dataset: self.start_from, self.dataset_folder, self.results_folder, self.csv_filename
1043
+
1044
+ # === OpenRouter API client ===
1045
+ self.client = OpenAI(
1046
+ base_url="https://openrouter.ai/api/v1",
1047
+ api_key=self.api_key,
1048
+ )
1049
+ self.extra_headers = {
1050
+ # Ganti Referer agar sesuai dengan Hugging Face Space Anda nanti
1051
+ "HTTP-Referer": "https://huggingface.co/spaces/[your-username]/[your-space-name]",
1052
+ "X-Title": f"Deepfake Detection Gradio ({self.model_name.upper()})"
1053
+ }
1054
+
1055
+ # === Model map (5 LLMs via OpenRouter) ===
1056
+ self.model_map = {
1057
+ "qwen": "qwen/qwen3-vl-8b-instruct",
1058
+ "gpt": "openai/chatgpt-4o-latest",
1059
+ "gemini": "google/gemini-2.5-flash",
1060
+ "llama": "meta-llama/llama-3.2-90b-vision-instruct",
1061
+ "cohere": "cohere/command-r-plus-08-2024",
1062
+ }
1063
+
1064
+ if self.model_name not in self.model_map:
1065
+ raise ValueError("❌ Invalid model name. Choose from: qwen, gpt, gemini, llama, cohere")
1066
+
1067
+ logger.info(f"Model selected: {self.model_name.upper()} ({self.model_map[self.model_name]})")
1068
+
1069
+ # Hapus os.makedirs(self.results_folder, exist_ok=True) karena tidak menyimpan hasil.
1070
+
1071
+ # PENTING: use_face_detector sekarang harus diinisialisasi berdasarkan input
1072
+ self.use_face_detector = use_face_detector and RETINAFACE_AVAILABLE
1073
+ self.target_size = 512
1074
+
1075
+ # Hapus: Properti terkait Adaptive delay (self.delay, self.fail_count, self.success_count)
1076
+
1077
+ # print(f"\nModel: {self.model_name.upper()} | RetinaFace Cropping: {'ON' if self.use_face_detector else 'OFF'}")
1078
+
1079
+ # === Image handling (RetinaFace) ===
1080
+ # FUNGSI INI TETAP TIDAK BERUBAH
1081
+ def preprocess_image_with_retinaface(self, image_path):
1082
+ if not self.use_face_detector or not RETINAFACE_AVAILABLE:
1083
+ return self.encode_image_simple(image_path)
1084
+
1085
+ try:
1086
+ faces = RetinaFace.detect_faces(str(image_path))
1087
+ if not faces:
1088
+ logger.warning(f"No face detected in {image_path}")
1089
+ return self.encode_image_simple(image_path)
1090
+
1091
+ first_face = list(faces.values())[0]
1092
+ facial_area = first_face.get("facial_area", None)
1093
+ if not facial_area or len(facial_area) != 4:
1094
+ return self.encode_image_simple(image_path)
1095
+
1096
+ x1, y1, x2, y2 = facial_area
1097
+ img = Image.open(image_path).convert("RGB")
1098
+
1099
+ # Tambahkan margin (ekstraksi)
1100
+ margin_ratio = 0.2
1101
+ w, h = x2 - x1, y2 - y1
1102
+ margin_x = int(w * margin_ratio)
1103
+ margin_y = int(h * margin_ratio)
1104
+
1105
+ x1 = max(0, x1 - margin_x)
1106
+ y1 = max(0, y1 - margin_y)
1107
+ x2 = min(img.width, x2 + margin_x)
1108
+ y2 = min(img.height, y2 + margin_y)
1109
+
1110
+ cropped_img = img.crop((x1, y1, x2, y2))
1111
+ cropped_img = cropped_img.resize((self.target_size, self.target_size), Image.Resampling.LANCZOS)
1112
+
1113
+ buf = io.BytesIO()
1114
+ cropped_img.save(buf, format="JPEG", quality=90, optimize=True)
1115
+ encoded = base64.b64encode(buf.getvalue()).decode("utf-8")
1116
+ return f"data:image/jpeg;base64,{encoded}"
1117
+
1118
+ except Exception as e:
1119
+ logger.error(f"RetinaFace error on {image_path}: {e}")
1120
+ return self.encode_image_simple(image_path)
1121
+
1122
+ # FUNGSI INI TETAP TIDAK BERUBAH
1123
+ def encode_image_simple(self, image_path):
1124
+ try:
1125
+ with open(image_path, "rb") as f:
1126
+ encoded = base64.b64encode(f.read()).decode("utf-8")
1127
+ return f"data:image/jpeg;base64,{encoded}"
1128
+ except Exception as e:
1129
+ logger.error(f"Encode error: {e}")
1130
+ return None
1131
+
1132
+ # FUNGSI INI TETAP TIDAK BERUBAH
1133
+ def validate_image(self, image_path):
1134
+ try:
1135
+ if not os.path.exists(image_path):
1136
+ return False
1137
+ with Image.open(image_path) as img:
1138
+ img.verify()
1139
+ return True
1140
+ except Exception:
1141
+ return False
1142
+
1143
+ # FUNGSI INI TETAP TIDAK BERUBAH
1144
+ def normalize_output(self, content):
1145
+ """
1146
+ Normalizes verbose LLM output to a single word: REAL, FAKE, or UNKNOWN.
1147
+ """
1148
+ if not content:
1149
+ return "UNKNOWN"
1150
+
1151
+ text = content.strip().upper()
1152
+
1153
+ if any(w in text for w in ["FAKE", "DEEPFAKE", "AI GENERATED", "SYNTHETIC"]):
1154
+ return "FAKE"
1155
+
1156
+ if any(w in text for w in ["REAL", "GENUINE", "HUMAN", "NOT FAKE"]):
1157
+ return "REAL"
1158
+
1159
+ words = text.split()
1160
+ if words:
1161
+ for word in words[:3]:
1162
+ if "REAL" in word: return "REAL"
1163
+ if "FAKE" in word: return "FAKE"
1164
+
1165
+ logger.warning(f"Output ambiguous/unpredictable: {content}")
1166
+ return "UNKNOWN"
1167
+
1168
+ # FUNGSI INI TETAP TIDAK BERUBAH
1169
+ def reverify_qwen(self, img_b64, prev_result):
1170
+ prompt = (
1171
+ "Re-analyze this face image for deepfake signs. "
1172
+ "Focus on lighting, symmetry, and unnatural skin artifacts. "
1173
+ "Respond with one word only: REAL or FAKE."
1174
+ )
1175
+ # print(f"🔁 Re-verifying Qwen result (was {prev_result})...") # Komen untuk lingkungan UI
1176
+ try:
1177
+ resp = self.client.chat.completions.create(
1178
+ extra_headers=self.extra_headers,
1179
+ model=self.model_map["qwen"],
1180
+ messages=[{
1181
+ "role": "user",
1182
+ "content": [
1183
+ {"type": "image_url", "image_url": {"url": img_b64}},
1184
+ {"type": "text", "text": prompt}
1185
+ ]
1186
+ }],
1187
+ max_tokens=50,
1188
+ temperature=0.1,
1189
+ )
1190
+ content = resp.choices[0].message.content.strip().upper()
1191
+ if "FAKE" in content:
1192
+ # print("Changed to FAKE after second check")
1193
+ return "FAKE"
1194
+ elif "REAL" in content:
1195
+ # print("Confirmed REAL after second check")
1196
+ return "REAL"
1197
+ else:
1198
+ # print("Still ambiguous after recheck")
1199
+ return prev_result
1200
+ except Exception as e:
1201
+ # print(f"Qwen re-verification failed: {e}")
1202
+ return prev_result
1203
+
1204
+ # === Deteksi utama ===
1205
+ # FUNGSI INI TETAP TIDAK BERUBAH (Inti dari Single Inference)
1206
+ def detect_deepfake_llm(self, image_path):
1207
+ prompt = (
1208
+ "You are a forensic image analyst. Analyze this face image for any deepfake or AI manipulation. "
1209
+ "Consider lighting, eyes, skin, and blending. Respond with only one word: REAL or FAKE."
1210
+ )
1211
+
1212
+ if not self.validate_image(image_path):
1213
+ return "ERROR", None, "invalid"
1214
+
1215
+ img_b64 = self.preprocess_image_with_retinaface(image_path)
1216
+ if not img_b64:
1217
+ return "ERROR", None, "invalid"
1218
+
1219
+ model_id = self.model_map[self.model_name]
1220
+ method = "retinaface_crop" if self.use_face_detector else "original"
1221
+
1222
+ try:
1223
+ resp = self.client.chat.completions.create(
1224
+ extra_headers=self.extra_headers,
1225
+ model=model_id,
1226
+ messages=[{
1227
+ "role": "user",
1228
+ "content": [
1229
+ {"type": "image_url", "image_url": {"url": img_b64}},
1230
+ {"type": "text", "text": prompt}
1231
+ ]
1232
+ }],
1233
+ max_tokens=50,
1234
+ temperature=0.1,
1235
+ )
1236
+ content = resp.choices[0].message.content
1237
+ result = self.normalize_output(content)
1238
+
1239
+ if self.model_name == "qwen" and result == "REAL":
1240
+ result = self.reverify_qwen(img_b64, result)
1241
+
1242
+ return result, content, method
1243
+
1244
+ except Exception as e:
1245
+ logger.error(f"Detection failed: {e}")
1246
+ return "ERROR", None, "API_FAILURE"
1247
+
1248
+ # === Dataset & Resume ===
1249
+ # HAPUS SEMUA FUNGSI BERIKUT: get_images, load_existing_results, save_results_to_csv, adjust_delay, run_detection
1250
+ # Fungsi-fungsi tersebut tidak diperlukan untuk aplikasi web Gradio
1251
+ pass # Pass diletakkan di sini hanya sebagai penanda bahwa sisanya telah dihapus.
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ openai
3
+ Pillow
4
+ tqdm
5
+ numpy
6
+ retina-face