Spaces:
Sleeping
Sleeping
| """ | |
| app.py | |
| ====== | |
| Flask backend za web aplikaciju za detekciju AI generiranog koda. | |
| Pokretanje: | |
| python app.py | |
| API rute: | |
| GET /api/health — provjera radi li server | |
| POST /api/analyze — analiza jednog isječka koda | |
| POST /api/analyze-batch — analiza više isječaka odjednom | |
| POST /api/similarity — međusobna sličnost više kodova | |
| React frontend šalje zahtjeve na ove rute i prikazuje rezultate. | |
| """ | |
| import os | |
| import json | |
| import difflib | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| from flask import Flask, request, jsonify, send_from_directory | |
| from flask_cors import CORS | |
| from classifier import predict, ucitaj_model | |
| from feature_extraction import extract_all_features, analyze_lines | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # INICIJALIZACIJA | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| app = Flask(__name__) | |
| # Putanja do buildan React aplikacije (postoji samo u produkciji) | |
| FRONTEND_BUILD = os.path.join(os.path.dirname(__file__), "frontend", "dist") | |
| # CORS dopušta React frontendu (localhost:3000) da komunicira s ovim serverom | |
| CORS(app, resources={r"/api/*": {"origins": "*"}}) | |
| # Učitaj model jednom pri pokretanju servera | |
| # (Ne učitavamo za svaki zahtjev — to bi bilo presporo) | |
| print("Učitavam model...") | |
| MODEL, SCALER, FEATURE_NAMES, THRESHOLD = ucitaj_model() | |
| if MODEL is None: | |
| print("UPOZORENJE: Model nije pronađen.") | |
| print("Pokreni prvo: python classifier.py") | |
| else: | |
| print(f"Model učitan ({len(FEATURE_NAMES)} značajki).") | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # POMOĆNE FUNKCIJE | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def greska(poruka: str, status: int = 400): | |
| """Vraća JSON odgovor s greškom.""" | |
| return jsonify({"error": poruka}), status | |
| def izracunaj_slicnost(kod_a: str, kod_b: str) -> float: | |
| """ | |
| Računa sličnost između dva isječka koda kao broj između 0.0 i 1.0. | |
| Koristi SequenceMatcher koji gleda zajednične podnizove. | |
| 0.0 = potpuno različiti kodovi | |
| 1.0 = identični kodovi | |
| Ovo je korisno za otkrivanje je li više studenata koristilo isti AI prompt — | |
| tada će njihovi kodovi biti međusobno neobično slični. | |
| """ | |
| return difflib.SequenceMatcher(None, kod_a.strip(), kod_b.strip()).ratio() | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # RUTE | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def health(): | |
| """ | |
| Provjera radi li server i je li model učitan. | |
| React frontend poziva ovo pri pokretanju da provjeri | |
| može li komunicirati sa serverom. | |
| Odgovor: | |
| { | |
| "status": "ok", | |
| "model_loaded": true, | |
| "feature_count": 40 | |
| } | |
| """ | |
| return jsonify({ | |
| "status": "ok", | |
| "model_loaded": MODEL is not None, | |
| "feature_count": len(FEATURE_NAMES) if FEATURE_NAMES else 0, | |
| "threshold": round(THRESHOLD, 3) if THRESHOLD else 0.65, | |
| }) | |
| def analyze(): | |
| """ | |
| Analizira jedan isječak koda i vraća procjenu AI podrijetla. | |
| Zahtjev (JSON): | |
| { | |
| "code": "def foo(x): return x", ← obavezno | |
| "language": "python", ← opcionalno | |
| "filename": "main.py" ← opcionalno | |
| } | |
| Odgovor (JSON): | |
| { | |
| "ai_probability": 0.73, | |
| "verdict": "Vjerojatno AI", | |
| "detected_language": "python", | |
| "top_features": [ | |
| {"name": "avg_identifier_length", "value": 8.5, "importance": 0.084}, | |
| ... | |
| ], | |
| "all_features": { ... }, | |
| "error": null | |
| } | |
| """ | |
| podaci = request.get_json(silent=True) | |
| if not podaci: | |
| return greska("Zahtjev mora sadržavati JSON tijelo.") | |
| kod = podaci.get("code", "").strip() | |
| if not kod: | |
| return greska("Polje 'code' je obavezno i ne smije biti prazno.") | |
| if len(kod) > 100_000: | |
| return greska("Kod je predugačak. Maksimalno 100.000 znakova.") | |
| jezik = podaci.get("language") | |
| datoteka = podaci.get("filename") | |
| rezultat = predict( | |
| code=kod, | |
| language=jezik, | |
| filename=datoteka, | |
| model=MODEL, | |
| scaler=SCALER, | |
| feature_names=FEATURE_NAMES, | |
| threshold=THRESHOLD, | |
| ) | |
| # Dodaj anotacije sumnjivih linija za prikaz u code editoru | |
| rezultat["line_annotations"] = analyze_lines( | |
| code=kod, | |
| language=jezik, | |
| filename=datoteka, | |
| ) | |
| return jsonify(rezultat) | |
| def analyze_batch(): | |
| """ | |
| Analizira više isječaka koda — streaming verzija. | |
| Umjesto da čeka sve rezultate pa ih pošalje odjednom (što uzrokuje | |
| timeout kod velikog broja fajlova), šalje svaki rezultat čim je gotov | |
| kao Server-Sent Events (SSE) stream. | |
| Frontend čita stream liniju po liniju i ažurira UI u realnom vremenu. | |
| Format streama — svaka linija je JSON objekt jedne od ove dvije vrste: | |
| {"type": "result", "data": { ...rezultat za jedan fajl... }} | |
| {"type": "summary", "data": { ...ukupna statistika... }} | |
| """ | |
| podaci = request.get_json(silent=True) | |
| if not podaci: | |
| return greska("Zahtjev mora sadržavati JSON tijelo.") | |
| submissions = podaci.get("submissions", []) | |
| if not submissions: | |
| return greska("Polje 'submissions' je prazno.") | |
| def generate(): | |
| rezultati = [] | |
| for sub in submissions: | |
| sub_id = sub.get("id", "nepoznat") | |
| kod = sub.get("code", "").strip() | |
| jezik = sub.get("language") | |
| datoteka = sub.get("filename") | |
| if not kod: | |
| rez = {"id": sub_id, "error": "Prazni kod."} | |
| else: | |
| try: | |
| rez = predict( | |
| code=kod, | |
| language=jezik, | |
| filename=datoteka, | |
| model=MODEL, | |
| scaler=SCALER, | |
| feature_names=FEATURE_NAMES, | |
| threshold=THRESHOLD, | |
| ) | |
| rez["id"] = sub_id | |
| rez["line_annotations"] = analyze_lines( | |
| code=kod, | |
| language=jezik, | |
| filename=datoteka, | |
| ) | |
| except Exception as e: | |
| rez = {"id": sub_id, "error": str(e)} | |
| rezultati.append(rez) | |
| # Pošalji rezultat odmah — ne čekamo ostale | |
| yield json.dumps({"type": "result", "data": rez}, | |
| ensure_ascii=False) + "\n" | |
| # Na kraju pošalji summary | |
| probs = [ | |
| r["ai_probability"] | |
| for r in rezultati | |
| if r.get("ai_probability") is not None | |
| ] | |
| summary = { | |
| "total": len(rezultati), | |
| "high_risk": sum(1 for p in probs if p >= 0.70), | |
| "medium_risk": sum(1 for p in probs if 0.40 <= p < 0.70), | |
| "low_risk": sum(1 for p in probs if p < 0.40), | |
| "avg_ai_probability": round(sum(probs) / len(probs), 4) if probs else 0.0, | |
| } | |
| yield json.dumps({"type": "summary", "data": summary}, | |
| ensure_ascii=False) + "\n" | |
| from flask import stream_with_context | |
| return app.response_class( | |
| stream_with_context(generate()), | |
| mimetype="application/x-ndjson", | |
| headers={ | |
| "X-Accel-Buffering": "no", # isključi nginx buffering ako postoji | |
| "Cache-Control": "no-cache", | |
| } | |
| ) | |
| def similarity(): | |
| """ | |
| Računa međusobnu sličnost između više kodova i vraća matricu sličnosti. | |
| Ovo otkriva je li više studenata predalo gotovo identičan kod | |
| — što sugerira korištenje istog AI prompta. | |
| Zahtjev (JSON): | |
| { | |
| "submissions": [ | |
| {"id": "student_01", "code": "..."}, | |
| {"id": "student_02", "code": "..."}, | |
| ... | |
| ] | |
| } | |
| Odgovor (JSON): | |
| { | |
| "ids": ["student_01", "student_02", ...], | |
| "matrix": [ | |
| [1.00, 0.87, 0.12, ...], ← sličnost student_01 s ostalima | |
| [0.87, 1.00, 0.15, ...], | |
| ... | |
| ], | |
| "suspicious_pairs": [ | |
| { | |
| "id_a": "student_01", | |
| "id_b": "student_02", | |
| "similarity": 0.87 | |
| }, | |
| ... | |
| ] | |
| } | |
| suspicious_pairs sadrži sve parove sa sličnošću > 0.70 | |
| (konfigurabilno, trenutno 70%). | |
| """ | |
| podaci = request.get_json(silent=True) | |
| if not podaci: | |
| return greska("Zahtjev mora sadržavati JSON tijelo.") | |
| submissions = podaci.get("submissions", []) | |
| if len(submissions) < 2: | |
| return greska("Potrebna su najmanje 2 koda za usporedbu.") | |
| ids = [s.get("id", f"kod_{i}") for i, s in enumerate(submissions)] | |
| codes = [s.get("code", "") for s in submissions] | |
| n = len(codes) | |
| # Izgradi n×n matricu sličnosti | |
| # matrix[i][j] = sličnost između koda i i koda j | |
| matrica = [[0.0] * n for _ in range(n)] | |
| sumnjivi_parovi = [] | |
| for i in range(n): | |
| for j in range(n): | |
| if i == j: | |
| matrica[i][j] = 1.0 | |
| elif j > i: | |
| sim = izracunaj_slicnost(codes[i], codes[j]) | |
| matrica[i][j] = round(sim, 4) | |
| matrica[j][i] = round(sim, 4) | |
| # Označi kao sumnjivo ako je sličnost > 70% | |
| if sim > 0.70: | |
| sumnjivi_parovi.append({ | |
| "id_a": ids[i], | |
| "id_b": ids[j], | |
| "similarity": round(sim, 4), | |
| }) | |
| # Sortiraj sumnjive parove po sličnosti (najsličniji prvi) | |
| sumnjivi_parovi.sort(key=lambda x: x["similarity"], reverse=True) | |
| return jsonify({ | |
| "ids": ids, | |
| "matrix": matrica, | |
| "suspicious_pairs": sumnjivi_parovi, | |
| }) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # SERVIRANJE REACT FRONTENDA (produkcija) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def serve_frontend(path): | |
| """ | |
| Servira buildan React app za sve rute koje nisu /api/*. | |
| Potrebno za client-side routing (React Router). | |
| """ | |
| if os.path.isdir(FRONTEND_BUILD): | |
| target = os.path.join(FRONTEND_BUILD, path) | |
| if path and os.path.exists(target): | |
| return send_from_directory(FRONTEND_BUILD, path) | |
| return send_from_directory(FRONTEND_BUILD, "index.html") | |
| return jsonify({"error": "Frontend nije buildан. Pokreni: cd frontend && npm run build"}), 404 | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 5000)) | |
| print("\n" + "=" * 50) | |
| print(" AI Code Detector — Backend") | |
| print("=" * 50) | |
| print(f" Server pokrenut na: http://localhost:{port}") | |
| print(" API rute:") | |
| print(" GET /api/health") | |
| print(" POST /api/analyze") | |
| print(" POST /api/analyze-batch") | |
| print(" POST /api/similarity") | |
| print("\n Zaustavi server s Ctrl+C") | |
| print("=" * 50 + "\n") | |
| app.run(host="0.0.0.0", port=port, debug=False, threaded=True) | |