"""PlaigLab web app: upload a paper -> live forensic analysis against world literature -> similarity + AI report. Run: python webapp/server.py """ import os import sys import threading import traceback import uuid from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.responses import JSONResponse, Response from fastapi.staticfiles import StaticFiles ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) from plagdetect import store # noqa: E402 from plagdetect.webpipeline import analyze_document # noqa: E402 UPLOAD_DIR = os.path.join(ROOT, "data", "uploads") STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") ALLOWED = (".pdf", ".txt", ".docx") app = FastAPI(title="PlaigLab") JOBS = {} def _run_job(job_id, path, depth): job = JOBS[job_id] def prog(msg): job["log"].append(msg) try: job["status"] = "running" job["result"] = analyze_document(path, progress=prog, depth=depth) job["status"] = "done" try: store.save_case(job["result"]) # persist for case history except Exception: traceback.print_exc() except Exception as exc: # surfaced to the UI traceback.print_exc() job["status"] = "error" job["error"] = str(exc) def _start(path, filename, depth="deep"): job_id = uuid.uuid4().hex[:12] JOBS[job_id] = {"status": "queued", "log": [], "result": None, "error": None, "filename": filename} threading.Thread(target=_run_job, args=(job_id, path, depth), daemon=True).start() return job_id @app.post("/api/analyze") async def analyze(file: UploadFile = File(...), depth: str = "deep"): ext = os.path.splitext(file.filename or "")[1].lower() if ext not in ALLOWED: raise HTTPException(400, f"Only {', '.join(ALLOWED)} files are supported") os.makedirs(UPLOAD_DIR, exist_ok=True) job_id = uuid.uuid4().hex[:6] dest = os.path.join(UPLOAD_DIR, f"{job_id}_{os.path.basename(file.filename)}") with open(dest, "wb") as f: f.write(await file.read()) return {"job_id": _start(dest, file.filename, depth)} @app.post("/api/sample") def sample(depth: str = "deep"): path = os.path.join(ROOT, "data", "test_real.txt") if not os.path.exists(path): raise HTTPException(404, "sample document missing") return {"job_id": _start(path, "sample_paper.txt", depth)} @app.get("/api/jobs/{job_id}") def job_status(job_id: str): job = JOBS.get(job_id) if not job: raise HTTPException(404, "unknown job") return {"status": job["status"], "log": job["log"], "error": job["error"], "filename": job["filename"]} @app.get("/api/jobs/{job_id}/report") def job_report(job_id: str): job = JOBS.get(job_id) if not job or job["status"] != "done": raise HTTPException(404, "report not ready") return JSONResponse(job["result"]) @app.get("/api/jobs/{job_id}/manifest") def job_manifest(job_id: str): """Downloadable coverage manifest (R2 — 'kya kya scrape kiya' full list).""" job = JOBS.get(job_id) if not job or job["status"] != "done": raise HTTPException(404, "manifest not ready") r = job["result"] payload = {"case_id": r["case_id"], "filename": r["filename"], "title": r["title"], "verdict": r["verdict"], "similarity_index": r["similarity_index"], "ai_score": r["ai_score"], "coverage": r["coverage"]} return JSONResponse(payload, headers={ "Content-Disposition": f'attachment; filename="manifest_{r["case_id"]}.json"'}) def _result_for(job_id): job = JOBS.get(job_id) if job and job["status"] == "done": return job["result"] return store.get_case(job_id) # fall back to persisted history @app.get("/api/jobs/{job_id}/pdf") def job_pdf(job_id: str): """Downloadable forensic PDF report.""" r = _result_for(job_id) if not r: raise HTTPException(404, "report not ready") from plagdetect.pdfreport import build_pdf pdf = build_pdf(r) return Response(pdf, media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="plaiglab_{r.get("case_id", job_id)}.pdf"'}) @app.get("/api/cases") def list_cases(): """Past-case history (persisted across restarts).""" return {"cases": store.list_cases()} @app.get("/api/cases/{case_id}") def get_case(case_id: str): r = store.get_case(case_id) if not r: raise HTTPException(404, "unknown case") return JSONResponse(r) app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") if __name__ == "__main__": import uvicorn # Host/port are env-driven so the same entrypoint works locally and on # container hosts (Hugging Face Spaces routes to 0.0.0.0:7860). host = os.environ.get("HOST", "127.0.0.1") port = int(os.environ.get("PORT", "8230")) uvicorn.run(app, host=host, port=port)