Dhiraj20 commited on
Commit
3745abe
·
1 Parent(s): 6cd9ea1

Deploy AI Backend Engine to HF Spaces

Browse files
Files changed (4) hide show
  1. .gitignore +32 -0
  2. Dockerfile +26 -0
  3. main.py +350 -0
  4. requirements.txt +11 -0
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .venv/
6
+ env/
7
+ venv/
8
+ ENV/
9
+ *.db
10
+ *.sqlite3
11
+
12
+ # Node.js
13
+ node_modules/
14
+ dist/
15
+ .env
16
+ .env.local
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+
22
+ # Models & Large Files
23
+ *.pth
24
+ *.h5
25
+ *.bin
26
+ *.exe
27
+ *.zip
28
+ *.docx
29
+ *.pdf
30
+
31
+ # Other Projects
32
+ SE-Lab-Election-Commission-and-Political-Party/
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies for OpenCV
4
+ USER root
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Create a user to avoid running as root (Required by Hugging Face)
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ WORKDIR $HOME/app
17
+
18
+ # Copy files and ensure the new user owns them
19
+ COPY --chown=user . $HOME/app
20
+
21
+ RUN pip install --no-cache-dir --user -r requirements.txt
22
+
23
+ # Hugging Face Spaces always listens on port 7860
24
+ EXPOSE 7860
25
+
26
+ CMD ["python", "main.py"]
main.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import time
4
+ import base64
5
+ import cv2
6
+ import numpy as np
7
+ import tempfile
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from fastapi.responses import HTMLResponse
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.templating import Jinja2Templates
13
+ from fastapi import Request
14
+ from transformers import pipeline
15
+ from PIL import Image
16
+ import torch
17
+ import torchvision.transforms as transforms
18
+ import torchvision.models as models
19
+ import sqlite3
20
+ from datetime import datetime
21
+ from pydantic import BaseModel
22
+
23
+ app = FastAPI(title="Deepfake Detection API")
24
+
25
+ # Setup CORS
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # Setup Templates (assuming your index.html is in 'templates' folder)
35
+ templates = Jinja2Templates(directory="templates")
36
+
37
+ # =====================================================================
38
+ # LOCAL HUGGING FACE MODEL SETUP (NO API KEY REQUIRED)
39
+ # =====================================================================
40
+ MODEL_ID = "haywoodsloan/ai-image-detector-deploy"
41
+ print(f"Loading local Hugging Face model '{MODEL_ID}'... This may take a moment to download weights on first run.")
42
+ # Load the model entirely locally (downloads weights to your machine)
43
+ local_hf_pipeline = pipeline("image-classification", model=MODEL_ID)
44
+ print("Model loaded successfully!")
45
+
46
+ ALLOWED_IMAGE_EXT = {"jpg", "jpeg", "png", "webp"}
47
+ ALLOWED_VIDEO_EXT = {"mp4", "avi", "mov", "mkv"}
48
+
49
+ cache = {}
50
+
51
+ # =====================================================================
52
+ # LOCAL MODEL SETUP (FOR WHEN YOU DOWNLOAD YOUR KAGGLE MODEL)
53
+ # =====================================================================
54
+ LOCAL_MODEL_PATH = "deepfake_resnet50.pth"
55
+ local_model = None
56
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
57
+
58
+ # Image transformations for the local PyTorch model
59
+ local_transform = transforms.Compose([
60
+ transforms.Resize((224, 224)),
61
+ transforms.ToTensor(),
62
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
63
+ ])
64
+
65
+ def load_local_model():
66
+ global local_model
67
+ if os.path.exists(LOCAL_MODEL_PATH):
68
+ print("Loading local PyTorch model...")
69
+ import torch.nn as nn
70
+ # Must match the architecture in kaggle_train.py
71
+ model = models.resnet50(pretrained=False)
72
+ num_ftrs = model.fc.in_features
73
+ model.fc = nn.Linear(num_ftrs, 2)
74
+ model.load_state_dict(torch.load(LOCAL_MODEL_PATH, map_location=device))
75
+ model.to(device)
76
+ model.eval()
77
+ local_model = model
78
+ print("Local model loaded successfully!")
79
+ else:
80
+ print(f"Local model not found at {LOCAL_MODEL_PATH}. Will use HuggingFace API if available.")
81
+
82
+ # Try to load local model on startup
83
+ load_local_model()
84
+
85
+ # =====================================================================
86
+ # DATABASE SETUP FOR COMMUNITY REPORTS
87
+ # =====================================================================
88
+ def init_db():
89
+ conn = sqlite3.connect("community.db")
90
+ cursor = conn.cursor()
91
+ cursor.execute("""
92
+ CREATE TABLE IF NOT EXISTS reports (
93
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
94
+ filename TEXT,
95
+ prediction TEXT,
96
+ confidence REAL,
97
+ image_base64 TEXT,
98
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
99
+ )
100
+ """)
101
+ conn.commit()
102
+ conn.close()
103
+
104
+ init_db()
105
+
106
+ # Pydantic Models for new endpoints
107
+ class ReportRequest(BaseModel):
108
+ filename: str
109
+ prediction: str
110
+ confidence: float
111
+ image_base64: str
112
+
113
+ class ChatRequest(BaseModel):
114
+ message: str
115
+
116
+ # =====================================================================
117
+ # ROUTES
118
+ # =====================================================================
119
+
120
+ @app.get("/", response_class=HTMLResponse)
121
+ async def home(request: Request):
122
+ return templates.TemplateResponse("index.html", {"request": request})
123
+
124
+ def is_allowed_file(filename: str, allowed_set: set):
125
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_set
126
+
127
+ def pil_to_jpeg_bytes(pil_img, max_side=800):
128
+ w, h = pil_img.size
129
+ crop = min(max_side, w, h)
130
+ img = pil_img.crop(((w-crop)//2, (h-crop)//2, (w+crop)//2, (h+crop)//2))
131
+ buf = io.BytesIO()
132
+ img.save(buf, format="JPEG", quality=95)
133
+ return buf.getvalue()
134
+
135
+ def image_to_base64_preview(pil_img, max_side=400):
136
+ img = pil_img.copy()
137
+ img.thumbnail((max_side, max_side))
138
+ buf = io.BytesIO()
139
+ img.save(buf, format="JPEG", quality=80)
140
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
141
+
142
+ # --- INFERENCE ENGINE ---
143
+ def classify_image(pil_img: Image.Image) -> dict:
144
+ """Uses either the local Kaggle model or the HuggingFace API."""
145
+
146
+ # 1. Try Local Model First
147
+ if local_model is not None:
148
+ input_tensor = local_transform(pil_img).unsqueeze(0).to(device)
149
+ with torch.no_grad():
150
+ outputs = local_model(input_tensor)
151
+ probabilities = torch.nn.functional.softmax(outputs[0], dim=0)
152
+
153
+ # Assuming class 0 is Real, class 1 is Fake (from kaggle_train.py)
154
+ real_score = probabilities[0].item()
155
+ fake_score = probabilities[1].item()
156
+
157
+ is_ai = fake_score > real_score
158
+ top_score = max(real_score, fake_score)
159
+
160
+ return {
161
+ "is_ai": is_ai,
162
+ "top_score": top_score,
163
+ "real_score": real_score,
164
+ "fake_score": fake_score,
165
+ }
166
+
167
+ # 2. Use Local Hugging Face Pipeline
168
+ img_bytes = pil_to_jpeg_bytes(pil_img)
169
+ key = hash(img_bytes)
170
+ if key in cache:
171
+ return cache[key]
172
+
173
+ # Run inference completely locally on your CPU/GPU
174
+ results = local_hf_pipeline(pil_img)
175
+
176
+ top_pred = max(results, key=lambda x: x["score"])
177
+ pred_label = top_pred["label"].lower()
178
+
179
+ # Detect fake/AI labels using the exact finalized logic
180
+ is_ai = any(
181
+ word in pred_label
182
+ for word in [
183
+ "fake",
184
+ "generated",
185
+ "artificial",
186
+ "deepfake",
187
+ "ai"
188
+ ]
189
+ )
190
+
191
+ # Calculate individual scores for the frontend
192
+ real_score = next((r["score"] for r in results if not any(w in r["label"].lower() for w in ["fake", "generated", "artificial", "deepfake", "ai"])), 0)
193
+ fake_score = next((r["score"] for r in results if any(w in r["label"].lower() for w in ["fake", "generated", "artificial", "deepfake", "ai"])), 0)
194
+
195
+ result = {
196
+ "is_ai": is_ai,
197
+ "top_score": top_pred["score"],
198
+ "real_score": real_score,
199
+ "fake_score": fake_score,
200
+ }
201
+
202
+ cache[key] = result
203
+ return result
204
+
205
+ @app.post("/predict-image")
206
+ async def predict_image(file: UploadFile = File(...)):
207
+ if not is_allowed_file(file.filename, ALLOWED_IMAGE_EXT):
208
+ raise HTTPException(status_code=400, detail="Invalid image extension")
209
+
210
+ start = time.time()
211
+ contents = await file.read()
212
+ img = Image.open(io.BytesIO(contents)).convert("RGB")
213
+
214
+ try:
215
+ scores = classify_image(img)
216
+ except Exception as e:
217
+ raise HTTPException(status_code=500, detail=str(e))
218
+
219
+ return {
220
+ "prediction": "AI-GENERATED" if scores["is_ai"] else "REAL",
221
+ "label": "fake" if scores["is_ai"] else "real",
222
+ "confidence": round(scores["top_score"] * 100, 1),
223
+ "probabilities": {
224
+ "real": round(scores["real_score"] * 100, 1),
225
+ "fake": round(scores["fake_score"] * 100, 1),
226
+ },
227
+ "image_preview": image_to_base64_preview(img),
228
+ "inference_time_ms": int((time.time() - start) * 1000),
229
+ "filename": file.filename,
230
+ "demo_mode": False
231
+ }
232
+
233
+ @app.post("/predict-video")
234
+ async def predict_video(file: UploadFile = File(...)):
235
+ if not is_allowed_file(file.filename, ALLOWED_VIDEO_EXT):
236
+ raise HTTPException(status_code=400, detail="Invalid video extension")
237
+
238
+ start = time.time()
239
+
240
+ # Save uploaded video to temp file
241
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
242
+ contents = await file.read()
243
+ tmp.write(contents)
244
+ path = tmp.name
245
+
246
+ cap = cv2.VideoCapture(path)
247
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
248
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
249
+
250
+ frames = []
251
+ # Extract 5 evenly spaced frames
252
+ idxs = np.linspace(0, max(total - 1, 0), 5, dtype=int)
253
+
254
+ for i in idxs:
255
+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(i))
256
+ ret, frame = cap.read()
257
+ if not ret:
258
+ continue
259
+
260
+ pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
261
+
262
+ try:
263
+ scores = classify_image(pil)
264
+ frames.append({
265
+ "frame_index": int(i),
266
+ "timestamp": round(i / fps, 2),
267
+ "prediction": "AI-GENERATED" if scores["is_ai"] else "REAL",
268
+ "label": "fake" if scores["is_ai"] else "real",
269
+ "confidence": round(scores["top_score"] * 100, 1)
270
+ })
271
+ except Exception as e:
272
+ print(f"Error processing frame {i}: {e}")
273
+
274
+ cap.release()
275
+ os.unlink(path)
276
+
277
+ if not frames:
278
+ raise HTTPException(status_code=500, detail="Could not extract any frames from video.")
279
+
280
+ fake_count = sum(1 for f in frames if f["label"] == "fake")
281
+ pct = round(fake_count / len(frames) * 100, 1)
282
+
283
+ return {
284
+ "overall_prediction": "AI-GENERATED" if pct >= 50 else "REAL",
285
+ "overall_label": "fake" if pct >= 50 else "real",
286
+ "fake_percentage": pct,
287
+ "real_percentage": 100 - pct,
288
+ "frames": frames,
289
+ "total_frames_analyzed": len(frames),
290
+ "inference_time_ms": int((time.time() - start) * 1000)
291
+ }
292
+
293
+ # --- COMMUNITY ENDPOINTS ---
294
+ @app.post("/submit-report")
295
+ async def submit_report(req: ReportRequest):
296
+ try:
297
+ conn = sqlite3.connect("community.db")
298
+ cursor = conn.cursor()
299
+ cursor.execute(
300
+ "INSERT INTO reports (filename, prediction, confidence, image_base64) VALUES (?, ?, ?, ?)",
301
+ (req.filename, req.prediction, req.confidence, req.image_base64)
302
+ )
303
+ conn.commit()
304
+ conn.close()
305
+ return {"status": "success", "message": "Report submitted to community database."}
306
+ except Exception as e:
307
+ raise HTTPException(status_code=500, detail=str(e))
308
+
309
+ @app.get("/community-reports")
310
+ async def get_community_reports():
311
+ try:
312
+ conn = sqlite3.connect("community.db")
313
+ conn.row_factory = sqlite3.Row
314
+ cursor = conn.cursor()
315
+ cursor.execute("SELECT * FROM reports ORDER BY timestamp DESC LIMIT 20")
316
+ rows = cursor.fetchall()
317
+ conn.close()
318
+ return [dict(row) for row in rows]
319
+ except Exception as e:
320
+ raise HTTPException(status_code=500, detail=str(e))
321
+
322
+ # --- CHATBOT ENDPOINT ---
323
+ @app.post("/chat")
324
+ async def chat_endpoint(req: ChatRequest):
325
+ msg = req.message.lower()
326
+
327
+ # Very simple keyword-based FAQ bot
328
+ if "how" in msg and ("work" in msg or "detect" in msg):
329
+ ans = "Our system uses advanced neural networks (Vision Transformers and ResNet-50) to analyze image patches for microscopic inconsistencies introduced by AI generators."
330
+ elif "accuracy" in msg or "accurate" in msg:
331
+ ans = "The models achieve over 95% accuracy on standard deepfake datasets by detecting blending artifacts and frequency domain anomalies."
332
+ elif "model" in msg or "architecture" in msg:
333
+ ans = "We use a dual-model approach: A Vision Transformer (ViT) via Hugging Face and a custom ResNet-50 PyTorch model trained on Kaggle."
334
+ elif "video" in msg:
335
+ ans = "For videos, we extract evenly spaced frames and analyze each one individually. If more than 50% of the frames are flagged, the entire video is considered AI-generated."
336
+ elif "hello" in msg or "hi" in msg:
337
+ ans = "Hello! I'm the NeuralEye Assistant. Ask me how our deepfake detection works, what models we use, or how to interpret your results!"
338
+ elif "report" in msg or "database" in msg:
339
+ ans = "If you detect an AI-generated image, you can report it to our Community Database! This helps warn others about fake media circulating online."
340
+ else:
341
+ ans = "I'm still learning! I can answer questions about how our deepfake detection works, the models we use, and how to analyze images/videos."
342
+
343
+ return {"reply": ans}
344
+
345
+ # Run the server using: uvicorn main:app --reload
346
+ if __name__ == "__main__":
347
+ import uvicorn
348
+ # Use the PORT environment variable if available, otherwise default to 7860 for HF Spaces
349
+ port = int(os.environ.get("PORT", 7860))
350
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.103.0
2
+ uvicorn>=0.23.2
3
+ python-multipart>=0.0.6
4
+ Pillow>=10.0.0
5
+ numpy>=1.24.0
6
+ torch>=2.0.0
7
+ torchvision>=0.15.0
8
+ opencv-python-headless>=4.8.0
9
+ huggingface-hub>=0.17.0
10
+ Jinja2>=3.1.2
11
+ transformers>=4.30.0