Yashodhar29's picture
Update app.py
8fd1d04 verified
Raw
History Blame Contribute Delete
26.1 kB
# server.py - DOUBLE FALLBACK ARCHITECTURE (SAPLING -> 7D | SIGHTENGINE -> CONVNEXT)
import os
import io
import gc
import cv2
import math
import uuid
import shutil
import joblib
import zipfile
import numpy as np
import torch
import torch.nn.functional as F
import timm
from collections import Counter
from typing import Optional
import requests
# API & Image Handling
from fastapi import FastAPI, HTTPException, UploadFile, File, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from PIL import Image
from torchvision import transforms
from skimage.measure import shannon_entropy
from docx import Document
import nltk
# Transformers & Hub
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from huggingface_hub import hf_hub_download, list_repo_files
# ==========================================================
# 0. NLTK SETUP
# ==========================================================
try:
nltk.data.find('tokenizers/punkt')
nltk.data.find('tokenizers/punkt_tab')
except LookupError:
nltk.download('punkt')
nltk.download('punkt_tab')
# ==========================================================
# 1. CONFIGURATION & EXTERNAL APIs
# ==========================================================
# --- TEXT API: Sapling ---
SAPLING_URL = "https://api.sapling.ai/api/v1/aidetect"
SAPLING_API_KEYS = [
"LFKP2ESNCF82CCX07XBOIY26ZMFT4QEO",
"4ZB9X067IWMVXRTJLKNVJKVDWQZEXHZ8",
"AECPJC27LX68E5L6O5X2V75S95IWUWKK",
"S5D99OJYA6TXRX49JJWKQUJP8P3YWYTH",
"QDB1FMQQZXOUJYRISG1G0K7QJGMPFTT4",
"YUUPMELNIJ424Z9H8F3NSG3529Q1VB86",
"85DNYDUMEASIHNQLSTHRCBJCN5B7S8C9",
"AT74JLK6VSJRR5O033OR73T39ABUQG6E",
"K1RG0FXGPQYXXMSM2K9BFISYJFZ5ZMWQ",
"ATXVUCTJCRQKWUG2P5S15A9EEA0BPAW3",
"57ND7C0EOHMC6YW9B89T0BYLYXVTO5VG",
"XAZKMDU879F6KX2RWSTHRZ1X0XLQD5BK",
"KA91IC2RKH68Z78CLJ4YDGDYU3BK9FIF",
"SPS0WY0377ZRIM3IKATC7MX5G8XDPFU8",
"NXTFWMCK47C7N0SMVK2HTGRBN00608E4",
"C8UX45IZMPADBFYR8FWKN9GB92TODQO6",
"MOMKK9QJ9W3SEDILWPMKSEH36EBK7XI6",
"YYYUILP6BWHG6XD83QQ0Y3IJT431VOD6",
"FVZ9FBFYFP0E93DHFDRH75V82F1TIFPW",
"YJ99AR2XVVR581LX4W93KCW59ZNQOW9O",
"47YR5OFIGWT3N9LS2VUL6Z9MY32XG3B1",
"A9YHY7ARNOU46AJOYBYM3HZGGKRHTKLX",
"86S3VGGQMCEF54R0IBEPT3N8RVLU6CFU",
"0VGCCYKM9H5PWTPHT7YOK1GGUDV77MO3",
"XU45624B3O6PEEMAT7BY7FNQB3U5W6MR",
"TKGDY124QDM20ERR67MP171GJ8KEVU07",
"V0G6NYIE13S8C8G0KOBI8I7GJLP8TPUX",
"76MB73YTW8N4XV3N66ZC3Y6XJL9BEAJ6",
"E2AIR6LCQO38PHTHXRQSOBP0P8Z28X58",
"U4ZIOTZ1K72TB6V5VXU9BLZE3NQYPTVS",
"PRZQS3EFQZ9253Q62VUTTUKRVJH5UJ5W",
"N5VU02OZHXHEAGT7AUKNI41UD9GJB8LY",
]
current_sapling_idx = 0
# --- IMAGE API: Sightengine ---
SIGHTENGINE_URL = "https://api.sightengine.com/1.0/check.json"
SIGHTENGINE_API_KEYS = [
{"user": "1743982661", "secret": "ZEZwGjcsyvq6ULyKgj5456SzNscWtKB8"},
{"user": "1638959208", "secret": "sei2oE3bbvzXsm5Nfs8tujdpbXCqETjb"},
{"user": "1882880449", "secret": "LyHJkg6qWAEztt8CVrPsyNRxR2y97H7s"},
{"user": "1776948743", "secret": "CXVznzPQU9P55AduTna4LggSrpHDYCrL"},
{"user": "1168060613", "secret": "8wRJKanHWkPLjnhDKb6GspdzMRcoiB7X"},
{"user": "20376699", "secret": "ruavg86UNL3wn8chGKidNX7spYDAJgrL"},
{"user": "1525731149", "secret": "JfjjCJJzdyGWbaxWioHWDGGqPSWkskKc"},
{"user": "482807524", "secret": "sFjXFDyBTiuyh33C57jWicfLVsPGh35W"},
{"user": "1806137219", "secret": "HcXtBp7rXBCBZExQzXRnPWo7HrAx7Mon"},
{"user": "1705951192", "secret": "9W7xT9AUiKJjeCiG6Q5CjT7zYHdQefJB"},
{"user": "1632737246", "secret": "PBEeTJVPXAJMhYUgnebzm4p2RrGh4BQz"},
{"user": "857335768", "secret": "bAqSCXTBRHUEy8E3vdH42JXf2rABpVKo"},
{"user": "1147336463", "secret": "2AKvLQN4VW33QDGvq6tY598SRwRYhGHu"},
{"user": "820176459", "secret": "F3SGYgiztjjCSMJo2DV4yHVWEpeTLzzx"},
{"user": "493725389", "secret": "HmsjtxMp9CSzfASkWTtK3zFYHPXaF6zK"},
{"user": "91559601", "secret": "F2KMUdDPq322fT9aDkPmbZjgjqxSHqef"},
{"user": "1478152006", "secret": "bVJ6xtikepzxrVcKKPRdMKL4h7aWbeek"},
{"user": "361978076", "secret": "ShjdhXtXuk9jMjGBMek4hDZuyZXheNNF"},
{"user": "355220504", "secret": "kBcuc7RSYoAqxqmQkzwFUbH5wvBD8Gwr"},
{"user": "1524398481", "secret": "5mZzcBiSdmf5KMSrUDXFojU6zJdNUeKr"},
{"user": "1751980311", "secret": "JuCLmEDBQvYw6ojDHiHgpm2YuRPWxCH3"},
{"user": "411151581", "secret": "Dm6GNEQtWMMDCPKY6wAb8X73x8BxeSg6"},
{"user": "576637210", "secret": "JTYDsX9iDZdifKoZ9uzhbMUSENVazFqs"},
{"user": "106682960", "secret": "8J6EuKLN5Pue6smijuBdqqAQX3kaYbGd"},
{"user": "1826450561", "secret": "jqnGBwYSHnyj8jsvwc6gA7yFbU3UpX5T"},
]
current_sightengine_idx = 0
# --- Models ---
TEXT_MODEL_1_ID = "Yuvrajg2107/deberta-v3-hybrid-detector_v2_universal"
TEXT_MODEL_2_ID = "Yuvrajg2107/roberta-base-cpp-final"
TEXT_MODEL_3_ID = "Yuvrajg2107/electra-large-discriminator-cpp-final"
ENSEMBLE_PATH = "model_ensemble_pro.pkl"
MARATHI_MAHABERT_ID = "Yashodhar29/mahabert-marathi-detection"
MARATHI_INDICBERT_ID = "Yashodhar29/indicbert-marathi-detection"
MARATHI_ENSEMBLE_PATH = "marathi_ensemble_pro.pkl"
CODE_MODEL_ID = "Yashodhar29/Qwen2.5-Coder-0.5B-Instruct-cpp"
# The Fallback Image Model
IMAGE_REPO_ID = "Yashodhar29/ConvNext-large-cpp"
# --- Device Setup ---
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"🚀 Server starting on device: {device.upper()}")
# ==========================================================
# 2. MODEL LOADING INFRASTRUCTURE
# ==========================================================
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# Global Model Storage
models = {
"text": [],
"code": None,
"marathi": None,
"image": None # Local ConvNeXt Fallback
}
def load_text_model(model_id):
print(f" ⏳ Loading {model_id}...")
try:
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForSequenceClassification.from_pretrained(
model_id,
trust_remote_code=True,
low_cpu_mem_usage=True
).to(device)
model.eval()
return {"model": model, "tokenizer": tokenizer, "name": model_id}
except Exception as e:
print(f" ❌ Failed to load {model_id}: {e}")
return None
def load_image_model_from_hub(repo_id):
print(f" ⏳ Loading Fallback Image Repo: {repo_id}...")
try:
files = list_repo_files(repo_id)
pth_files = [f for f in files if f.endswith('.pth')]
if not pth_files: return None
weights_path = hf_hub_download(repo_id=repo_id, filename=pth_files[0])
model = timm.create_model("convnext_large.fb_in22k_ft_in1k", pretrained=False, num_classes=2)
state_dict = torch.load(weights_path, map_location=device)
model.load_state_dict(state_dict)
model.to(device)
model.eval()
print(" ✅ ConvNeXt Fallback Ready.")
return model
except Exception as e:
print(f" ❌ Image Model Error: {e}")
return None
# --- INITIALIZATION ---
print("\n⚙️ --- LOADING HEAVY MODELS INTO MEMORY ---")
# 1. Load Text Models (7D Fallback)
models["text"].append(load_text_model(TEXT_MODEL_1_ID))
models["text"].append(load_text_model(TEXT_MODEL_2_ID))
models["text"].append(load_text_model(TEXT_MODEL_3_ID))
try:
models["ensemble"] = joblib.load(ENSEMBLE_PATH)
print(" ✅ English 7D Ensemble Loaded.")
except Exception:
models["ensemble"] = None
# 2. Load Marathi Models
marathi_components = {}
try:
marathi_components['ensemble'] = joblib.load(MARATHI_ENSEMBLE_PATH) if os.path.exists(MARATHI_ENSEMBLE_PATH) else None
marathi_components['tok_maha'] = AutoTokenizer.from_pretrained(MARATHI_MAHABERT_ID, trust_remote_code=True)
marathi_components['mod_maha'] = AutoModelForSequenceClassification.from_pretrained(MARATHI_MAHABERT_ID, trust_remote_code=True, low_cpu_mem_usage=True).to(device)
marathi_components['mod_maha'].eval()
marathi_components['tok_indic'] = AutoTokenizer.from_pretrained(MARATHI_INDICBERT_ID, trust_remote_code=True)
marathi_components['mod_indic'] = AutoModelForSequenceClassification.from_pretrained(MARATHI_INDICBERT_ID, trust_remote_code=True, low_cpu_mem_usage=True).to(device)
marathi_components['mod_indic'].eval()
models["marathi"] = marathi_components
print(" ✅ Marathi System Ready!")
except Exception:
models["marathi"] = None
# 3. Load Code Model
try:
models["code"] = {
"tokenizer": AutoTokenizer.from_pretrained(CODE_MODEL_ID, trust_remote_code=True),
"model": AutoModelForSequenceClassification.from_pretrained(CODE_MODEL_ID, trust_remote_code=True, low_cpu_mem_usage=True).to(device)
}
models["code"]["model"].eval()
print(" ✅ Code Model Ready.")
except Exception:
pass
# 4. Load Image Fallback Model
models["image"] = load_image_model_from_hub(IMAGE_REPO_ID)
gc.collect()
if device == "cuda": torch.cuda.empty_cache()
# ==========================================================
# 3. HELPER FUNCTIONS
# ==========================================================
def get_stylometric_features(text):
if not text: return [0,0,0,0]
prob = [float(text.count(c)) / len(text) for c in dict.fromkeys(list(text))]
entropy = - sum([p * math.log(p) / math.log(2.0) for p in prob])
sentences = text.replace('!', '.').replace('?', '.').split('.')
lengths = [len(s.split()) for s in sentences if len(s.split()) > 0]
burstiness = np.std(lengths) if lengths else 0
words = text.lower().split()
ttr = len(set(words)) / len(words) if words else 0
if len(words) < 3: ngram_ratio = 0
else:
ngrams = list(zip(*[words[i:] for i in range(3)]))
counts = Counter(ngrams)
repeated = sum(1 for count in counts.values() if count > 1)
ngram_ratio = repeated / len(ngrams)
return [entropy, burstiness, ttr, ngram_ratio]
def get_prob(text, tokenizer, model):
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(device)
with torch.no_grad():
outputs = model(**inputs)
probs = F.softmax(outputs.logits, dim=-1)
return probs[0][1].item()
def get_marathi_features(text):
if not text: return [0.0] * 8
sentences = nltk.sent_tokenize(text)
words = nltk.word_tokenize(text)
num_words = len(words)
if num_words == 0: return [0.0] * 8
num_sentences = max(len(sentences), 1)
avg_sentence_len = num_words / num_sentences
sentence_lengths = [len(nltk.word_tokenize(s)) for s in sentences]
std_sentence_len = np.std(sentence_lengths) if len(sentence_lengths) > 1 else 0.0
ttr = len(set(words)) / num_words
counts = Counter(text)
entropy = -sum((c/len(text)) * math.log2(c/len(text)) for c in counts.values())
word_counts = Counter(words)
hapax_ratio = sum(1 for c in word_counts.values() if c == 1) / num_words
avg_word_len = len(text) / num_words
burstiness = (std_sentence_len / avg_sentence_len) if avg_sentence_len > 0 else 0.0
return [avg_sentence_len, std_sentence_len, ttr, entropy, hapax_ratio, avg_word_len, burstiness, (entropy * avg_sentence_len)]
def get_forensics(img_pil):
img_np = np.array(img_pil)
if len(img_np.shape) == 2: img_np = cv2.cvtColor(img_np, cv2.COLOR_GRAY2RGB)
elif img_np.shape[2] == 4: img_np = cv2.cvtColor(img_np, cv2.COLOR_RGBA2RGB)
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
dft = np.fft.fft2(gray)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = np.log(np.abs(dft_shift) + 1)
return {
"spectral_artifacts": round(float(np.mean(magnitude_spectrum)), 3),
"perplexity": round(float(shannon_entropy(gray)), 3),
"burstiness": round(float(np.std(cv2.Canny(gray, 100, 200))), 3)
}
def get_image_transforms():
return transforms.Compose([
transforms.Resize((384, 384)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# --- MASTER IMAGE PIPELINE (API + LOCAL FALLBACK) ---
def analyze_image_pipeline(pil_img, filename="image.jpg"):
global current_sightengine_idx
keys_tried = 0
total_keys = len(SIGHTENGINE_API_KEYS)
# 1. Prepare for API
img_bytes = io.BytesIO()
pil_img.save(img_bytes, format='JPEG')
# 2. Try Sightengine API Rotation
while keys_tried < total_keys:
creds = SIGHTENGINE_API_KEYS[current_sightengine_idx]
try:
img_bytes.seek(0)
files = {'media': (filename, img_bytes, 'image/jpeg')}
params = {'models': 'genai', 'api_user': creds['user'], 'api_secret': creds['secret']}
response = requests.post(SIGHTENGINE_URL, files=files, data=params, timeout=20)
if 200 <= response.status_code < 300:
result = response.json()
if result.get("status") == "success":
ai_score = result.get('type', {}).get('ai_generated', 0.0)
return float(ai_score), f"sightengine_api (Key {current_sightengine_idx + 1})"
else:
current_sightengine_idx = (current_sightengine_idx + 1) % total_keys
keys_tried += 1
else:
current_sightengine_idx = (current_sightengine_idx + 1) % total_keys
keys_tried += 1
except Exception:
current_sightengine_idx = (current_sightengine_idx + 1) % total_keys
keys_tried += 1
# 3. Trigger Local ConvNeXt Fallback
print(f"❌ Sightengine APIs exhausted/failed for {filename}. Triggering ConvNeXt Fallback.")
if models.get("image") is not None:
try:
transform = get_image_transforms()
img_t = transform(pil_img).unsqueeze(0).to(device)
with torch.no_grad():
logits = models["image"](img_t)
probs = F.softmax(logits, dim=1)
# Standard ConvNeXt Index 0 is often AI. Adjust if your model expects Index 1.
fallback_score = probs[0][0].item()
return float(fallback_score), "local_convnext_fallback"
except Exception as e:
print(f"⚠️ ConvNeXt Fallback Failed: {e}")
return 0.0, "error_fallback"
# ==========================================================
# 4. API ENDPOINTS
# ==========================================================
class DetectionRequest(BaseModel):
text: str
@app.post("/analyze")
def analyze_text(request: DetectionRequest):
global current_sapling_idx
user_text = request.text
if len(user_text.strip()) < 5: return {"ai_score": 0, "label": "Too Short", "stats": {}}
if "def " in user_text and ("return" in user_text or "class" in user_text):
return {"ai_score": 0.0, "label": "Use /analyze_code endpoint", "stats": {}}
stats = get_stylometric_features(user_text)
final_prob = None
used_fallback = False
used_source = ""
api_success = False
keys_tried = 0
total_keys = len(SAPLING_API_KEYS)
while keys_tried < total_keys:
test_key = SAPLING_API_KEYS[current_sapling_idx]
try:
response = requests.post(SAPLING_URL, json={"key": test_key, "text": user_text}, timeout=20)
if 200 <= response.status_code < 300:
final_prob = response.json().get("score", 0.0)
used_source = f"sapling_api (Key {current_sapling_idx + 1})"
api_success = True
break
else:
current_sapling_idx = (current_sapling_idx + 1) % total_keys
keys_tried += 1
except Exception:
current_sapling_idx = (current_sapling_idx + 1) % total_keys
keys_tried += 1
if not api_success:
print("❌ Sapling APIs exhausted. Triggering Heavy 7D Fallback.")
used_fallback = True
if used_fallback or final_prob is None:
dl_probs = []
for entry in models["text"]:
if entry:
try:
inputs = entry["tokenizer"](user_text, return_tensors="pt", truncation=True, max_length=512).to(device)
with torch.no_grad():
outputs = entry["model"](**inputs)
dl_probs.append(F.softmax(outputs.logits, dim=-1)[0][1].item())
except Exception:
dl_probs.append(0.5)
else:
dl_probs.append(0.5)
final_prob = dl_probs[0]
if models["ensemble"]:
try:
final_prob = float(models["ensemble"].predict_proba(np.array([dl_probs + stats]))[0][1])
except: pass
used_source = "local_7D_ensemble"
gc.collect()
if device == "cuda": torch.cuda.empty_cache()
return {
"ai_score": round(float(final_prob), 4),
"label": "🤖 AI GENERATED" if final_prob > 0.5 else "👤 HUMAN WRITTEN",
"detailed_scores": {used_source: round(final_prob, 4)},
"stats": {
"entropy": round(stats[0], 2), "burstiness": round(stats[1], 2),
"ttr": round(stats[2], 2), "ngram_ratio": round(stats[3], 2)
}
}
@app.post("/analyze_marathi")
async def analyze_marathi(request: DetectionRequest):
text = request.text.strip()
if not text: return {"ai_score": 0, "label": "Empty Text", "stats": {}}
m_sys = models["marathi"]
if not m_sys: raise HTTPException(status_code=503, detail="Marathi models failed to load.")
try:
prob_maha = get_prob(text, m_sys['tok_maha'], m_sys['mod_maha'])
prob_indic = get_prob(text, m_sys['tok_indic'], m_sys['mod_indic'])
stats_features = get_marathi_features(text)
confidence = 0.0
if m_sys['ensemble']:
try:
confidence = float(m_sys['ensemble'].predict_proba(np.array([[prob_maha, prob_indic] + stats_features]))[0][1])
except:
confidence = 1.0 if m_sys['ensemble'].predict(np.array([[prob_maha, prob_indic] + stats_features]))[0] == 1 else 0.0
else:
confidence = (prob_maha + prob_indic) / 2.0
return {
"ai_score": round(confidence, 4),
"label": "🤖 AI GENERATED" if confidence > 0.5 else "👤 HUMAN WRITTEN",
"stats": {"avg_sent_len": round(stats_features[0], 2), "entropy": round(stats_features[3], 2)}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/analyze_code")
async def analyze_code(request: DetectionRequest):
if not models["code"]: raise HTTPException(status_code=503, detail="Code model not loaded.")
try:
inputs = models["code"]["tokenizer"](request.text, return_tensors="pt", truncation=True, max_length=512).to(device)
with torch.no_grad():
ai_prob = F.softmax(models["code"]["model"](**inputs).logits, dim=-1)[0][1].item()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
stats = get_stylometric_features(request.text)
return {
"ai_score": round(float(ai_prob), 4),
"label": "🤖 AI CODE" if ai_prob > 0.5 else "👤 HUMAN CODE",
"stats": {"entropy": round(stats[0], 2), "burstiness": round(stats[1], 2)}
}
@app.post("/analyze_image")
def analyze_image(file: UploadFile = File(...)):
try:
pil_img = Image.open(io.BytesIO(file.file.read())).convert('RGB')
forensics = get_forensics(pil_img)
# MAGIC HAPPENS HERE: Auto-Routes to API -> Falls back to ConvNeXt
ai_score, used_source = analyze_image_pipeline(pil_img, filename=file.filename)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
file.file.close()
return {
"ai_score": round(float(ai_score), 4),
"label": "AI Generated" if ai_score > 0.5 else "Real / Human",
"detailed_scores": {used_source: round(float(ai_score), 4)},
"forensics": forensics
}
@app.post("/analyze_video")
def analyze_video(file: UploadFile = File(...), num_samples: int = 10):
unique_name = f"temp_vid_{uuid.uuid4()}.mp4"
try:
with open(unique_name, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
cap = cv2.VideoCapture(unique_name)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if total_frames < 1: raise ValueError("Empty video")
indices = np.linspace(0, total_frames-1, num=min(num_samples, total_frames), dtype=int)
scores = []
for i in indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
ret, frame = cap.read()
if not ret: continue
pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# API -> Fallback
score, _ = analyze_image_pipeline(pil_img, filename=f"frame_{i}.jpg")
scores.append(score)
cap.release()
if not scores: return {"ai_score": 0, "label": "Error"}
avg_score = sum(scores) / len(scores)
return {
"ai_score": round(avg_score, 4),
"label": "AI Video" if avg_score > 0.5 else "Real Video",
"frames_analyzed": len(scores)
}
except Exception as e:
return {"error": str(e)}
finally:
if os.path.exists(unique_name): os.remove(unique_name)
file.file.close()
@app.post("/analyze_document")
def analyze_document(file: UploadFile = File(...)):
global current_sapling_idx
try:
content = file.file.read()
file_bytes = io.BytesIO(content)
try:
doc = Document(file_bytes)
full_text = "\n".join([para.text for para in doc.paragraphs])
except: full_text = ""
extracted_images = []
try:
file_bytes.seek(0)
with zipfile.ZipFile(file_bytes) as z:
image_files = [f for f in z.namelist() if f.startswith('word/media/')]
for img_path in image_files:
with z.open(img_path) as img_file:
try:
extracted_images.append({
"filename": os.path.basename(img_path),
"image_obj": Image.open(io.BytesIO(img_file.read())).convert('RGB')
})
except: continue
except: pass
text_result = None
if len(full_text.split()) > 5:
api_success = False
keys_tried = 0
t_prob = 0.0
while keys_tried < len(SAPLING_API_KEYS):
try:
response = requests.post(
SAPLING_URL,
json={"key": SAPLING_API_KEYS[current_sapling_idx], "text": full_text[:2000]},
timeout=20
)
if 200 <= response.status_code < 300:
t_prob = response.json().get("score", 0.0)
api_success = True
break
else:
current_sapling_idx = (current_sapling_idx + 1) % len(SAPLING_API_KEYS)
keys_tried += 1
except Exception:
current_sapling_idx = (current_sapling_idx + 1) % len(SAPLING_API_KEYS)
keys_tried += 1
if not api_success:
stats = get_stylometric_features(full_text)
dl_probs = []
for entry in models["text"]:
if entry:
try:
inputs = entry["tokenizer"](full_text[:2000], return_tensors="pt", truncation=True, max_length=512).to(device)
with torch.no_grad():
dl_probs.append(F.softmax(entry["model"](**inputs).logits, dim=-1)[0][1].item())
except: dl_probs.append(0.5)
else: dl_probs.append(0.5)
t_prob = dl_probs[0]
if models["ensemble"]:
try: t_prob = float(models["ensemble"].predict_proba(np.array([dl_probs + stats]))[0][1])
except: pass
text_result = {"ai_score": t_prob, "preview": full_text[:100]}
image_results = []
for item in extracted_images:
# API -> Fallback applied to documents too
img_ai_score, _ = analyze_image_pipeline(item['image_obj'], filename=item['filename'])
image_results.append({
"filename": item['filename'],
"ai_score": round(float(img_ai_score), 4),
"label": "AI Generated" if img_ai_score > 0.5 else "Real / Human",
"forensics": get_forensics(item['image_obj'])
})
scores = [text_result['ai_score']] if text_result else []
scores.extend([img['ai_score'] for img in image_results])
final_score = (sum(scores) / len(scores) + max(scores)) / 2 if scores else 0.0
return {
"type": "document_report",
"ai_score": round(final_score, 4),
"label": "AI DETECTED" if final_score > 0.5 else "HUMAN WRITTEN",
"text_analysis": text_result,
"image_analysis": image_results,
"summary": f"Analyzed {len(full_text.split())} words and {len(image_results)} images."
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Document parsing failed: {str(e)}")
finally:
file.file.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)