Yashodhar29 commited on
Commit
c184858
·
verified ·
1 Parent(s): 18d1f85

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +23 -0
  2. model_ensemble_pro.pkl +3 -0
  3. qwen_ensemble_brain.pkl +3 -0
  4. requirements.txt +15 -0
  5. server.py +447 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9
2
+ FROM python:3.9
3
+
4
+ # Set working directory
5
+ WORKDIR /code
6
+
7
+ # Copy files
8
+ COPY ./requirements.txt /code/requirements.txt
9
+
10
+ # Install dependencies
11
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
12
+
13
+ # Copy the rest of the code (server.py and pkl file)
14
+ COPY . /code
15
+
16
+ # Create a writable directory for cache (Fixes permission errors on HF Spaces)
17
+ RUN mkdir -p /code/cache
18
+ ENV TRANSFORMERS_CACHE=/code/cache
19
+ ENV HF_HOME=/code/cache
20
+ RUN chmod -R 777 /code/cache
21
+
22
+ # Start the server on port 7860 (Hugging Face default)
23
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
model_ensemble_pro.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2592025fd1bb13e6ea83014d40e740e39ecc085edecdeea6eaf8f93394a80656
3
+ size 2389403
qwen_ensemble_brain.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f4db89229e6c525aec11dcab85d1bea1a59433a06fc86236c56e0a349d4feaf4
3
+ size 2965663
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ torch
5
+ transformers
6
+ timm
7
+ numpy
8
+ scikit-learn
9
+ joblib
10
+ opencv-python-headless
11
+ pillow
12
+ scipy
13
+ scikit-image
14
+ python-docx
15
+ huggingface-hub
server.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server.py - 7-INPUT SUPER ENSEMBLE + DYNAMIC HUGGING FACE LOADING
2
+ import os
3
+ import io
4
+ import gc
5
+ import cv2
6
+ import math
7
+ import uuid
8
+ import shutil
9
+ import joblib
10
+ import zipfile
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn.functional as F
14
+ import timm
15
+ from collections import Counter
16
+ from typing import Optional
17
+
18
+ # API & Image Handling
19
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Query
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from pydantic import BaseModel
22
+ from PIL import Image
23
+ from torchvision import transforms
24
+ from skimage.measure import shannon_entropy
25
+ from scipy.stats import pearsonr
26
+ from docx import Document
27
+
28
+ # Transformers & Hub
29
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
30
+ from huggingface_hub import hf_hub_download, list_repo_files
31
+
32
+ # ==========================================================
33
+ # 1. CONFIGURATION & HUGGING FACE REPOS
34
+ # ==========================================================
35
+
36
+ # --- Text Models (The 3 Judges for the Ensemble) ---
37
+ TEXT_MODEL_1_ID = "Yuvrajg2107/deberta-v3-hybrid-detector_v2_universal"
38
+ TEXT_MODEL_2_ID = "Yuvrajg2107/roberta-base-cpp-final"
39
+ TEXT_MODEL_3_ID = "Yuvrajg2107/electra-large-discriminator-cpp-final"
40
+
41
+ # --- Code Model ---
42
+ CODE_MODEL_ID = "Yashodhar29/Qwen2.5-Coder-0.5B-Instruct-cpp"
43
+
44
+ # --- Image Model ---
45
+ IMAGE_REPO_ID = "Yashodhar29/ConvNext-large-cpp"
46
+ # We will dynamically find the .pth file in this repo later
47
+
48
+ # --- Local Ensemble File ---
49
+ ENSEMBLE_PATH = "model_ensemble_pro.pkl" # Ensure this is in your folder!
50
+
51
+ # --- Device Setup ---
52
+ device = "cuda" if torch.cuda.is_available() else "cpu"
53
+ print(f"🚀 Server starting on device: {device.upper()}")
54
+
55
+ # ==========================================================
56
+ # 2. MODEL LOADING INFRASTRUCTURE
57
+ # ==========================================================
58
+
59
+ app = FastAPI()
60
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
61
+
62
+ # Global Model Storage
63
+ models = {
64
+ "text": [],
65
+ "code": None,
66
+ "image": None,
67
+ "ensemble": None
68
+ }
69
+
70
+ def load_text_model(model_id):
71
+ """Loads a HF text model and tokenizer."""
72
+ print(f" ⏳ Loading {model_id}...")
73
+ try:
74
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
75
+ model = AutoModelForSequenceClassification.from_pretrained(model_id).to(device)
76
+ model.eval()
77
+ return {"model": model, "tokenizer": tokenizer, "name": model_id}
78
+ except Exception as e:
79
+ print(f" ❌ Failed to load {model_id}: {e}")
80
+ return None
81
+
82
+ def load_image_model_from_hub(repo_id):
83
+ """Downloads .pth from HF and loads into ConvNeXt."""
84
+ print(f" ⏳ Checking Image Repo: {repo_id}...")
85
+ try:
86
+ # 1. Find the .pth file dynamically
87
+ files = list_repo_files(repo_id)
88
+ pth_files = [f for f in files if f.endswith('.pth')]
89
+
90
+ if not pth_files:
91
+ print(" ❌ No .pth file found in image repo!")
92
+ return None
93
+
94
+ # Pick the first one (or prioritize 'best' if multiple)
95
+ weights_filename = pth_files[0]
96
+ print(f" ⬇️ Downloading weights: {weights_filename}")
97
+
98
+ weights_path = hf_hub_download(repo_id=repo_id, filename=weights_filename)
99
+
100
+ # 2. Create Architecture
101
+ model = timm.create_model("convnext_large.fb_in22k_ft_in1k", pretrained=False, num_classes=2)
102
+
103
+ # 3. Load Weights
104
+ state_dict = torch.load(weights_path, map_location=device)
105
+ model.load_state_dict(state_dict)
106
+ model.to(device)
107
+ model.eval()
108
+ print(" ✅ Image Model Ready.")
109
+ return model
110
+ except Exception as e:
111
+ print(f" ❌ Image Model Error: {e}")
112
+ return None
113
+
114
+ # --- INITIALIZATION ---
115
+ print("\n⚙️ --- LOADING MODELS ---")
116
+
117
+ # 1. Load Text Models (DeBERTa, RoBERTa, ELECTRA)
118
+ models["text"].append(load_text_model(TEXT_MODEL_1_ID))
119
+ models["text"].append(load_text_model(TEXT_MODEL_2_ID))
120
+ models["text"].append(load_text_model(TEXT_MODEL_3_ID))
121
+
122
+ # 2. Load Code Model (Qwen)
123
+ print(f" ⏳ Loading Code Model: {CODE_MODEL_ID}...")
124
+ try:
125
+ models["code"] = {
126
+ "tokenizer": AutoTokenizer.from_pretrained(CODE_MODEL_ID),
127
+ "model": AutoModelForSequenceClassification.from_pretrained(CODE_MODEL_ID).to(device)
128
+ }
129
+ models["code"]["model"].eval()
130
+ except Exception as e:
131
+ print(f" ❌ Code Model Failed: {e}")
132
+
133
+ # 3. Load Image Model (ConvNeXt)
134
+ models["image"] = load_image_model_from_hub(IMAGE_REPO_ID)
135
+
136
+ # 4. Load Scikit-Learn Ensemble
137
+ print(f" ⏳ Loading 'The Judge' ({ENSEMBLE_PATH})...")
138
+ try:
139
+ models["ensemble"] = joblib.load(ENSEMBLE_PATH)
140
+ print(" ✅ Ensemble Loaded (VotingClassifier).")
141
+ except Exception as e:
142
+ print(f" ⚠️ Ensemble Pickle Not Found or Invalid: {e}")
143
+ print(" ⚠️ Server will fall back to raw DeBERTa scores.")
144
+
145
+
146
+ # ==========================================================
147
+ # 3. HELPER FUNCTIONS
148
+ # ==========================================================
149
+
150
+ def get_stylometric_features(text):
151
+ if not text: return [0,0,0,0]
152
+
153
+ # 1. Entropy
154
+ prob = [float(text.count(c)) / len(text) for c in dict.fromkeys(list(text))]
155
+ entropy = - sum([p * math.log(p) / math.log(2.0) for p in prob])
156
+
157
+ # 2. Burstiness
158
+ sentences = text.replace('!', '.').replace('?', '.').split('.')
159
+ lengths = [len(s.split()) for s in sentences if len(s.split()) > 0]
160
+ burstiness = np.std(lengths) if lengths else 0
161
+
162
+ # 3. TTR (Type-Token Ratio)
163
+ words = text.lower().split()
164
+ ttr = len(set(words)) / len(words) if words else 0
165
+
166
+ # 4. N-Gram Repetition
167
+ if len(words) < 3: ngram_ratio = 0
168
+ else:
169
+ ngrams = list(zip(*[words[i:] for i in range(3)]))
170
+ counts = Counter(ngrams)
171
+ repeated = sum(1 for count in counts.values() if count > 1)
172
+ ngram_ratio = repeated / len(ngrams)
173
+
174
+ return [entropy, burstiness, ttr, ngram_ratio]
175
+
176
+ def get_image_transforms():
177
+ return transforms.Compose([
178
+ transforms.Resize((384, 384)),
179
+ transforms.ToTensor(),
180
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
181
+ ])
182
+
183
+ def get_forensics(img_pil):
184
+ """Calculates non-ML forensic metrics for images."""
185
+ img_np = np.array(img_pil)
186
+ gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
187
+
188
+ dft = np.fft.fft2(gray)
189
+ dft_shift = np.fft.fftshift(dft)
190
+ magnitude_spectrum = np.log(np.abs(dft_shift) + 1)
191
+
192
+ spectral_score = np.mean(magnitude_spectrum)
193
+ perplexity = shannon_entropy(gray)
194
+
195
+ edges = cv2.Canny(gray, 100, 200)
196
+ burstiness = np.std(edges)
197
+
198
+ return {
199
+ "spectral_artifacts": round(float(spectral_score), 3),
200
+ "perplexity": round(float(perplexity), 3),
201
+ "burstiness": round(float(burstiness), 3)
202
+ }
203
+
204
+ # ==========================================================
205
+ # 4. API ENDPOINTS
206
+ # ==========================================================
207
+
208
+ class DetectionRequest(BaseModel):
209
+ text: str
210
+
211
+ @app.post("/analyze")
212
+ async def analyze_text(request: DetectionRequest):
213
+ """
214
+ Main Text Detection Endpoint.
215
+ Uses the 7-Input Super Ensemble: [DeBERTa, RoBERTa, ELECTRA, Entropy, Burstiness, TTR, NGram]
216
+ """
217
+ user_text = request.text
218
+ if len(user_text.strip()) < 5:
219
+ return {"ai_score": 0, "label": "Too Short", "stats": {}}
220
+
221
+ # --- A. Check for Code (Routing) ---
222
+ # If text is actually code, route to simple logic or return early advice
223
+ if "def " in user_text and ("return" in user_text or "class" in user_text):
224
+ return {"ai_score": 0.0, "label": "Use /analyze_code endpoint", "stats": {}}
225
+
226
+ # --- B. Get DL Probabilities (The 3 Inputs) ---
227
+ dl_probs = []
228
+
229
+ # We rely on DeBERTa (Index 0) heavily, so if it fails, we abort.
230
+ if not models["text"][0]:
231
+ raise HTTPException(status_code=500, detail="Primary model (DeBERTa) not active.")
232
+
233
+ for entry in models["text"]:
234
+ if entry:
235
+ try:
236
+ inputs = entry["tokenizer"](user_text, return_tensors="pt", truncation=True, max_length=512).to(device)
237
+ with torch.no_grad():
238
+ outputs = entry["model"](**inputs)
239
+ probs = F.softmax(outputs.logits, dim=-1)
240
+ # Assume Index 1 is AI (standard for these models)
241
+ dl_probs.append(probs[0][1].item())
242
+ except Exception as e:
243
+ print(f"Inference Error on {entry['name']}: {e}")
244
+ dl_probs.append(0.5) # Neutral fallback
245
+ else:
246
+ dl_probs.append(0.5) # Missing model fallback
247
+
248
+ # --- C. Get Stylometry (The 4 Inputs) ---
249
+ stats = get_stylometric_features(user_text) # [Entropy, Burstiness, TTR, NGram]
250
+
251
+ # --- D. Final Ensemble Prediction ---
252
+ final_prob = dl_probs[0] # Default to DeBERTa if ensemble fails
253
+
254
+ if models["ensemble"]:
255
+ # Input Vector: [M1, M2, M3, Stat1, Stat2, Stat3, Stat4]
256
+ input_vector = np.array([dl_probs + stats])
257
+ try:
258
+ ensemble_probs = models["ensemble"].predict_proba(input_vector)
259
+ final_prob = ensemble_probs[0][1]
260
+ except Exception as e:
261
+ print(f"Ensemble Voting Failed: {e}")
262
+
263
+ return {
264
+ "ai_score": round(float(final_prob), 4),
265
+ "label": "🤖 AI GENERATED" if final_prob > 0.5 else "👤 HUMAN WRITTEN",
266
+ "detailed_scores": {
267
+ "deberta": round(dl_probs[0], 4),
268
+ "roberta": round(dl_probs[1], 4),
269
+ "electra": round(dl_probs[2], 4)
270
+ },
271
+ "stats": {
272
+ "entropy": round(stats[0], 2),
273
+ "burstiness": round(stats[1], 2),
274
+ "ttr": round(stats[2], 2),
275
+ "ngram_ratio": round(stats[3], 2)
276
+ }
277
+ }
278
+
279
+ @app.post("/analyze_code")
280
+ async def analyze_code(request: DetectionRequest):
281
+ """
282
+ Dedicated Code Detection using Qwen2.5-Coder.
283
+ """
284
+ if not models["code"]:
285
+ raise HTTPException(status_code=503, detail="Code model (Qwen) not loaded.")
286
+
287
+ user_code = request.text
288
+ try:
289
+ inputs = models["code"]["tokenizer"](user_code, return_tensors="pt", truncation=True, max_length=512).to(device)
290
+ with torch.no_grad():
291
+ outputs = models["code"]["model"](**inputs)
292
+ probs = F.softmax(outputs.logits, dim=-1)
293
+ ai_prob = probs[0][1].item()
294
+ except Exception as e:
295
+ raise HTTPException(status_code=500, detail=f"Code analysis failed: {e}")
296
+
297
+ # Basic stats for frontend display
298
+ stats = get_stylometric_features(user_code)
299
+
300
+ return {
301
+ "ai_score": round(float(ai_prob), 4),
302
+ "label": "🤖 AI CODE" if ai_prob > 0.5 else "👤 HUMAN CODE",
303
+ "stats": {
304
+ "entropy": round(stats[0], 2),
305
+ "burstiness": round(stats[1], 2)
306
+ }
307
+ }
308
+
309
+ @app.post("/analyze_image")
310
+ async def analyze_image(file: UploadFile = File(...)):
311
+ """
312
+ Image Detection using ConvNeXt-Large.
313
+ """
314
+ if not models["image"]:
315
+ raise HTTPException(status_code=503, detail="Image model not loaded.")
316
+
317
+ try:
318
+ contents = await file.read()
319
+ pil_img = Image.open(io.BytesIO(contents)).convert('RGB')
320
+
321
+ # 1. Forensic Stats
322
+ forensics = get_forensics(pil_img)
323
+
324
+ # 2. AI Detection
325
+ transform = get_image_transforms()
326
+ img_t = transform(pil_img).unsqueeze(0).to(device)
327
+
328
+ with torch.no_grad():
329
+ logits = models["image"](img_t)
330
+ probs = F.softmax(logits, dim=1)
331
+ ai_score = probs[0][0].item() # Check if label 0 is AI or Human based on your training.
332
+ # Usually Index 0 is AI in these datasets, but verify if inverted.
333
+
334
+ # Note: If your training had label 1 as AI, change to probs[0][1].
335
+ # Assuming standard label 0 = AI for ConvNeXt fine-tunes often used here.
336
+ # If your previous code assumed index 0 is AI, we keep that.
337
+
338
+ except Exception as e:
339
+ raise HTTPException(status_code=500, detail=f"Image processing error: {str(e)}")
340
+
341
+ return {
342
+ "ai_score": round(float(ai_score), 4),
343
+ "label": "AI Generated" if ai_score > 0.5 else "Real / Human",
344
+ "forensics": forensics
345
+ }
346
+
347
+ @app.post("/analyze_video")
348
+ async def analyze_video(file: UploadFile = File(...), num_samples: int = 10):
349
+ """
350
+ Video Frame Extraction + Analysis.
351
+ """
352
+ if not models["image"]:
353
+ raise HTTPException(status_code=503, detail="Image model needed for video.")
354
+
355
+ unique_name = f"temp_vid_{uuid.uuid4()}.mp4"
356
+ try:
357
+ with open(unique_name, "wb") as buffer:
358
+ shutil.copyfileobj(file.file, buffer)
359
+
360
+ cap = cv2.VideoCapture(unique_name)
361
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
362
+
363
+ if total_frames < 1:
364
+ raise ValueError("Empty video")
365
+
366
+ indices = np.linspace(0, total_frames-1, num=min(num_samples, total_frames), dtype=int)
367
+
368
+ scores = []
369
+ transform = get_image_transforms()
370
+
371
+ for i in indices:
372
+ cap.set(cv2.CAP_PROP_POS_FRAMES, i)
373
+ ret, frame = cap.read()
374
+ if not ret: continue
375
+
376
+ # Convert BGR (OpenCV) to RGB (PIL)
377
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
378
+ pil_img = Image.fromarray(frame_rgb)
379
+
380
+ img_t = transform(pil_img).unsqueeze(0).to(device)
381
+ with torch.no_grad():
382
+ logits = models["image"](img_t)
383
+ probs = F.softmax(logits, dim=1)
384
+ scores.append(probs[0][0].item()) # Using same index assumption as image
385
+
386
+ cap.release()
387
+
388
+ if not scores: return {"ai_score": 0, "label": "Error"}
389
+
390
+ avg_score = sum(scores) / len(scores)
391
+
392
+ return {
393
+ "ai_score": round(avg_score, 4),
394
+ "label": "AI Video" if avg_score > 0.5 else "Real Video",
395
+ "frames_analyzed": len(scores)
396
+ }
397
+
398
+ except Exception as e:
399
+ print(f"Video Error: {e}")
400
+ return {"error": str(e)}
401
+ finally:
402
+ if os.path.exists(unique_name):
403
+ os.remove(unique_name)
404
+
405
+ @app.post("/analyze_document")
406
+ async def analyze_document(file: UploadFile = File(...)):
407
+ """
408
+ Hybrid Document Analysis (Text + Images inside Doc).
409
+ """
410
+ try:
411
+ content = await file.read()
412
+ file_bytes = io.BytesIO(content)
413
+
414
+ # 1. Extract Text
415
+ try:
416
+ doc = Document(file_bytes)
417
+ full_text = "\n".join([para.text for para in doc.paragraphs])
418
+ except:
419
+ full_text = ""
420
+
421
+ # 2. Analyze Text
422
+ text_res = None
423
+ if len(full_text) > 50:
424
+ # Manually trigger the logic from /analyze
425
+ # For simplicity, we just take the raw Request object logic here or call internal function
426
+ # We will just do a quick manual run:
427
+
428
+ # Calc Probs
429
+ t_inputs = models["text"][0]["tokenizer"](full_text[:2000], return_tensors="pt", truncation=True, max_length=512).to(device)
430
+ with torch.no_grad():
431
+ t_out = models["text"][0]["model"](**t_inputs)
432
+ t_prob = F.softmax(t_out.logits, dim=-1)[0][1].item()
433
+
434
+ text_res = {"ai_score": t_prob, "preview": full_text[:100]}
435
+
436
+ return {
437
+ "type": "document_report",
438
+ "text_analysis": text_res,
439
+ "note": "Image extraction from docx disabled for brevity in this version."
440
+ }
441
+
442
+ except Exception as e:
443
+ raise HTTPException(status_code=500, detail=str(e))
444
+
445
+ if __name__ == "__main__":
446
+ import uvicorn
447
+ uvicorn.run(app, host="0.0.0.0", port=8000)