perplexity / app.py
UltimateEdge's picture
Upload 4 files
5a87951 verified
Raw
History Blame Contribute Delete
6.39 kB
import torch
import math
import re
import statistics
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
app = FastAPI(title="Perplexity + Burstiness API β€” UltimateEdge")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ── Load model at startup ───────────────────────────────────
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Loading GPT-2 medium on {device}...")
tokenizer = GPT2TokenizerFast.from_pretrained("gpt2-medium")
model = GPT2LMHeadModel.from_pretrained("gpt2-medium").to(device)
model.eval()
print("Model ready.")
# ── Request schema ──────────────────────────────────────────
class TextRequest(BaseModel):
text: str
# ── Core functions ──────────────────────────────────────────
def compute_perplexity(text: str) -> dict:
encodings = tokenizer(text, return_tensors="pt")
input_ids = encodings.input_ids.to(device)
seq_len = input_ids.size(1)
if seq_len == 0:
return {"error": "Empty text."}
stride, max_length = 512, 1024
nlls, prev_end = [], 0
for begin_loc in range(0, seq_len, stride):
end_loc = min(begin_loc + max_length, seq_len)
trg_len = end_loc - prev_end
chunk = input_ids[:, begin_loc:end_loc]
labels = chunk.clone()
labels[:, :-trg_len] = -100
with torch.no_grad():
loss = model(chunk, labels=labels).loss
nlls.append(loss * trg_len)
prev_end = end_loc
if end_loc == seq_len:
break
mean_nll = torch.stack(nlls).sum() / seq_len
ppl = torch.exp(mean_nll).item()
if ppl < 20: ppl_label = "Very Low β€” AI-like"
elif ppl < 50: ppl_label = "Low β€” fluent prose"
elif ppl < 100: ppl_label = "Moderate β€” human range"
elif ppl < 200: ppl_label = "High β€” complex/varied"
else: ppl_label = "Very High β€” noisy/technical"
return {
"perplexity": round(ppl, 2),
"bits_per_token": round(mean_nll.item() / math.log(2), 4),
"n_tokens": int(seq_len),
"interpretation": ppl_label,
}
def get_sentence_ppls(text: str) -> list:
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
results = []
for sent in sentences:
sent = sent.strip()
if len(sent.split()) < 3:
continue
try:
r = compute_perplexity(sent)
if "error" not in r:
results.append({"sentence": sent, "ppl": r["perplexity"], "tokens": r["n_tokens"]})
except Exception:
continue
return results
def compute_burstiness(ppl_scores: list) -> dict:
if len(ppl_scores) < 2:
return {"error": "Need at least 2 sentences."}
mean = statistics.mean(ppl_scores)
std = statistics.stdev(ppl_scores)
burstiness = (std - mean) / (std + mean) if (std + mean) != 0 else 0
if burstiness > 0.2: b_label = "Very High β€” strong human signature"
elif burstiness > 0.05: b_label = "High β€” likely human writing"
elif burstiness > -0.1: b_label = "Moderate β€” mixed signals"
elif burstiness > -0.3: b_label = "Low β€” leans AI-generated"
else: b_label = "Very Low β€” strong AI signature"
return {
"burstiness_score": round(burstiness, 4),
"std_dev": round(std, 2),
"mean_sentence_ppl": round(mean, 2),
"coefficient_of_variation_pct": round((std / mean * 100) if mean else 0, 1),
"interpretation": b_label,
}
def compute_verdict(ppl: float, burstiness: float) -> dict:
if ppl < 20: ppl_score = 5
elif ppl < 40: ppl_score = 25
elif ppl < 60: ppl_score = 55
elif ppl < 100: ppl_score = 75
elif ppl < 200: ppl_score = 85
else: ppl_score = 60
if burstiness > 0.3: b_score = 95
elif burstiness > 0.1: b_score = 80
elif burstiness > 0.0: b_score = 60
elif burstiness > -0.1: b_score = 40
elif burstiness > -0.3: b_score = 20
else: b_score = 5
human_score = round((ppl_score * 0.45) + (b_score * 0.55))
if human_score >= 75: verdict, detail = "βœ… Likely Human-Written", "Both metrics suggest natural, varied writing."
elif human_score >= 55: verdict, detail = "⚠️ Mixed β€” Possibly AI-Assisted", "Some human patterns but uniformity in places."
elif human_score >= 35: verdict, detail = "πŸ€– Leans AI-Generated", "Low burstiness and predictable structure."
else: verdict, detail = "πŸ€– Very Likely AI-Generated", "Strong AI signature across both metrics."
return {
"verdict": verdict,
"human_score": f"{human_score}/100",
"detail": detail,
"ppl_contribution": f"{ppl_score}/100",
"burstiness_contribution": f"{b_score}/100",
}
# ── Routes ──────────────────────────────────────────────────
@app.get("/")
def root():
return {
"name": "Perplexity + Burstiness API β€” UltimateEdge",
"usage": "POST /analyse with JSON body: {\"text\": \"your article here\"}",
"endpoints": ["/analyse", "/health"]
}
@app.get("/health")
def health():
return {"status": "ok", "device": device}
@app.post("/analyse")
def analyse(req: TextRequest):
text = req.text.strip()
if not text:
return {"error": "Empty text provided."}
overall = compute_perplexity(text)
if "error" in overall:
return overall
sent_results = get_sentence_ppls(text)
ppl_scores = [s["ppl"] for s in sent_results]
burst = compute_burstiness(ppl_scores) if len(ppl_scores) >= 2 else {"error": "Not enough sentences."}
b_val = burst.get("burstiness_score", 0)
verdict = compute_verdict(overall["perplexity"], b_val)
return {
"perplexity": overall,
"burstiness": burst,
"verdict": verdict,
"sentences": sorted(sent_results, key=lambda x: x["ppl"]),
}