| from flask import Blueprint, request, jsonify, send_file |
| from werkzeug.utils import secure_filename |
| from typing import Tuple |
| import os |
| import uuid |
| from datetime import datetime |
|
|
| from .models import ( |
| AnalyzeTextRequest, AnalysisResponse, ResultListResponse, |
| SummaryResponse, StatusResponse |
| ) |
| from backend.detectors import DetectorEnsemble |
| from backend.utils import FileParser, TextCleaner |
| from backend.database import DatabaseManager, Session |
| from backend.config.detectors_config import DetectorsConfig |
| from backend.config.settings import settings |
| from backend.utils.submission_logger import log_submission |
|
|
| |
| api = Blueprint('api', __name__, url_prefix='/api') |
|
|
| |
| _detector_ensemble = None |
|
|
| def get_detector_ensemble(): |
| """Get or initialize detector ensemble""" |
| global _detector_ensemble |
| if _detector_ensemble is None: |
| _detector_ensemble = DetectorEnsemble() |
| return _detector_ensemble |
|
|
| |
| |
| |
|
|
| @api.route('/health', methods=['GET']) |
| def health_check(): |
| """Health check endpoint""" |
| return jsonify({ |
| "status": "healthy", |
| "timestamp": datetime.utcnow().isoformat() |
| }), 200 |
|
|
| @api.route('/status', methods=['GET']) |
| def get_status(): |
| """Get system and detector status""" |
| try: |
| ensemble = get_detector_ensemble() |
| status_info = ensemble.get_status() |
| |
| return jsonify({ |
| "status": "ok", |
| "detectors": status_info, |
| "config": { |
| "database_url": settings.DATABASE_URL[:20] + "***", |
| "upload_folder": settings.UPLOAD_FOLDER, |
| } |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": str(e) |
| }), 500 |
|
|
| |
| |
| |
|
|
| @api.route('/analyze/text', methods=['POST']) |
| def analyze_text(): |
| """ |
| Analyze raw text for AI-generated content. |
| |
| POST /api/analyze/text |
| { |
| "text": "Your text here...", |
| "filename": "optional_name.txt" |
| } |
| """ |
| try: |
| data = request.get_json() |
| |
| if not data or 'text' not in data: |
| return jsonify({ |
| "status": "error", |
| "message": "Missing 'text' field in request" |
| }), 400 |
| |
| text = data.get('text', '').strip() |
| filename = data.get('filename', 'untitled.txt') |
| user_id = data.get('user_id') |
| |
| |
| if not text: |
| return jsonify({ |
| "status": "error", |
| "message": "Text cannot be empty" |
| }), 400 |
| |
| if len(text) < 10: |
| return jsonify({ |
| "status": "error", |
| "message": "Text must be at least 10 characters" |
| }), 400 |
| |
| |
| text = TextCleaner.clean(text) |
| |
| |
| text_stats = TextCleaner.get_text_stats(text) |
| |
| |
| ensemble = get_detector_ensemble() |
| ensemble_result = ensemble.detect(text) |
| |
| |
| detector_results_dict = ensemble_result.to_dict() |
| |
| db_result = DatabaseManager.save_analysis_result( |
| filename=filename, |
| file_format="raw", |
| file_size=len(text), |
| text_length=len(text), |
| word_count=text_stats['word_count'], |
| overall_ai_score=ensemble_result.overall_score, |
| overall_confidence=ensemble_result.overall_confidence, |
| detector_results=detector_results_dict, |
| text_preview=text[:500], |
| user_id=user_id, |
| ) |
| |
| if not db_result: |
| return jsonify({ |
| "status": "error", |
| "message": "Failed to save analysis result" |
| }), 500 |
| |
| status_label = db_result.get_status_label(ensemble_result.overall_score) |
| log_submission( |
| filename=filename, |
| overall_ai_score=ensemble_result.overall_score, |
| overall_confidence=ensemble_result.overall_confidence, |
| status_label=status_label, |
| detector_results=detector_results_dict["detector_results"], |
| text_stats=text_stats, |
| text_preview=text[:200], |
| ) |
|
|
| return jsonify({ |
| "status": "success", |
| "message": "Text analyzed successfully", |
| "result_id": db_result.id, |
| "file_id": db_result.file_id, |
| "overall_ai_score": round(ensemble_result.overall_score, 4), |
| "overall_ai_score_percentage": f"{ensemble_result.overall_score * 100:.1f}%", |
| "overall_confidence": ensemble_result.overall_confidence, |
| "status_label": status_label, |
| "detector_results": detector_results_dict["detector_results"], |
| "enabled_detectors": ensemble_result.enabled_detectors, |
| "text_stats": text_stats, |
| }), 200 |
| |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| return jsonify({ |
| "status": "error", |
| "message": "Error during analysis", |
| "error_details": str(e) |
| }), 500 |
|
|
| @api.route('/analyze/file', methods=['POST']) |
| def analyze_file(): |
| """ |
| Upload and analyze a file (PDF, DOCX, or TXT). |
| |
| POST /api/analyze/file |
| FormData: |
| - file: (required) File to upload |
| """ |
| try: |
| |
| if 'file' not in request.files: |
| return jsonify({ |
| "status": "error", |
| "message": "No file provided" |
| }), 400 |
| |
| file = request.files['file'] |
| user_id = request.form.get('user_id') |
| |
| if not file or not file.filename: |
| return jsonify({ |
| "status": "error", |
| "message": "Invalid file" |
| }), 400 |
| |
| |
| filename = secure_filename(file.filename) |
| file_ext = os.path.splitext(filename)[1].lower() |
| |
| if file_ext not in {'.pdf', '.docx', '.doc', '.txt'}: |
| return jsonify({ |
| "status": "error", |
| "message": f"Unsupported file format: {file_ext}. Supported: PDF, DOCX, TXT" |
| }), 400 |
| |
| |
| temp_filename = f"{uuid.uuid4()}{file_ext}" |
| temp_filepath = os.path.join(settings.UPLOAD_FOLDER, temp_filename) |
| file.save(temp_filepath) |
| |
| file_size = os.path.getsize(temp_filepath) |
| |
| |
| if file_size > settings.MAX_FILE_SIZE: |
| os.remove(temp_filepath) |
| return jsonify({ |
| "status": "error", |
| "message": f"File too large. Max size: {settings.MAX_FILE_SIZE / (1024*1024):.1f}MB" |
| }), 400 |
| |
| |
| text, file_format, parse_error = FileParser.parse_file(temp_filepath) |
| |
| if parse_error: |
| os.remove(temp_filepath) |
| return jsonify({ |
| "status": "error", |
| "message": f"Error parsing file: {str(parse_error)}" |
| }), 400 |
| |
| |
| if not text or len(text) < 10: |
| os.remove(temp_filepath) |
| return jsonify({ |
| "status": "error", |
| "message": "Could not extract sufficient text from file" |
| }), 400 |
| |
| |
| text = TextCleaner.clean(text) |
| text_stats = TextCleaner.get_text_stats(text) |
| |
| |
| ensemble = get_detector_ensemble() |
| ensemble_result = ensemble.detect(text) |
| |
| |
| detector_results_dict = ensemble_result.to_dict() |
| |
| db_result = DatabaseManager.save_analysis_result( |
| filename=filename, |
| file_format=file_format, |
| file_size=file_size, |
| text_length=len(text), |
| word_count=text_stats['word_count'], |
| overall_ai_score=ensemble_result.overall_score, |
| overall_confidence=ensemble_result.overall_confidence, |
| detector_results=detector_results_dict, |
| text_preview=text[:500], |
| user_id=user_id, |
| ) |
| |
| |
| os.remove(temp_filepath) |
| |
| if not db_result: |
| return jsonify({ |
| "status": "error", |
| "message": "Failed to save analysis result" |
| }), 500 |
| |
| status_label = db_result.get_status_label(ensemble_result.overall_score) |
| log_submission( |
| filename=filename, |
| overall_ai_score=ensemble_result.overall_score, |
| overall_confidence=ensemble_result.overall_confidence, |
| status_label=status_label, |
| detector_results=detector_results_dict["detector_results"], |
| text_stats=text_stats, |
| text_preview=text[:200], |
| ) |
|
|
| return jsonify({ |
| "status": "success", |
| "message": "File analyzed successfully", |
| "result_id": db_result.id, |
| "file_id": db_result.file_id, |
| "filename": filename, |
| "overall_ai_score": round(ensemble_result.overall_score, 4), |
| "overall_ai_score_percentage": f"{ensemble_result.overall_score * 100:.1f}%", |
| "overall_confidence": ensemble_result.overall_confidence, |
| "status_label": status_label, |
| "detector_results": detector_results_dict["detector_results"], |
| "enabled_detectors": ensemble_result.enabled_detectors, |
| "text_stats": text_stats, |
| }), 200 |
| |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| return jsonify({ |
| "status": "error", |
| "message": "Error analyzing file", |
| "error_details": str(e) |
| }), 500 |
|
|
| |
| |
| |
|
|
| @api.route('/results', methods=['GET']) |
| def list_results(): |
| """ |
| Get list of all analysis results with pagination. |
| |
| GET /api/results?page=1&limit=10&sort=recent |
| """ |
| try: |
| page = request.args.get('page', 1, type=int) |
| limit = request.args.get('limit', 10, type=int) |
| sort = request.args.get('sort', 'recent') |
| |
| |
| if page < 1: |
| page = 1 |
| if limit < 1 or limit > 100: |
| limit = 10 |
| |
| |
| order_by = "upload_timestamp_desc" if sort == "recent" else "score_desc" |
| |
| |
| offset = (page - 1) * limit |
| |
| |
| results = DatabaseManager.get_all_results( |
| limit=limit, |
| offset=offset, |
| order_by=order_by |
| ) |
| |
| |
| session = Session.get_session() |
| total_count = session.query(DatabaseManager).count() if hasattr(DatabaseManager, '__table__') else 0 |
| from backend.database import AnalysisResult |
| total_count = session.query(AnalysisResult).count() |
| session.close() |
| |
| result_dicts = [result.to_dict() for result in results] |
| |
| return jsonify({ |
| "status": "success", |
| "message": "Results retrieved", |
| "page": page, |
| "limit": limit, |
| "total_count": total_count, |
| "results": result_dicts |
| }), 200 |
| |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| return jsonify({ |
| "status": "error", |
| "message": "Error retrieving results", |
| "error_details": str(e) |
| }), 500 |
|
|
| @api.route('/results/<int:result_id>', methods=['GET']) |
| def get_result(result_id): |
| """Get a specific analysis result""" |
| try: |
| result = DatabaseManager.get_result_by_id(result_id) |
| |
| if not result: |
| return jsonify({ |
| "status": "error", |
| "message": "Result not found" |
| }), 404 |
| |
| return jsonify({ |
| "status": "success", |
| "message": "Result retrieved", |
| "result": result.to_dict() |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": "Error retrieving result", |
| "error_details": str(e) |
| }), 500 |
|
|
| @api.route('/results/<int:result_id>', methods=['DELETE']) |
| def delete_result(result_id): |
| """Delete an analysis result""" |
| try: |
| success = DatabaseManager.delete_result(result_id) |
| |
| if not success: |
| return jsonify({ |
| "status": "error", |
| "message": "Result not found" |
| }), 404 |
| |
| return jsonify({ |
| "status": "success", |
| "message": "Result deleted" |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": "Error deleting result", |
| "error_details": str(e) |
| }), 500 |
|
|
| @api.route('/results/<int:result_id>', methods=['PUT']) |
| def update_result(result_id): |
| """Update analysis result (notes, flags)""" |
| try: |
| data = request.get_json() |
| |
| updates = {} |
| if 'notes' in data: |
| updates['notes'] = data['notes'] |
| if 'is_flagged' in data: |
| updates['is_flagged'] = data['is_flagged'] |
| |
| if not updates: |
| return jsonify({ |
| "status": "error", |
| "message": "No fields to update" |
| }), 400 |
| |
| result = DatabaseManager.update_result(result_id, **updates) |
| |
| if not result: |
| return jsonify({ |
| "status": "error", |
| "message": "Result not found" |
| }), 404 |
| |
| return jsonify({ |
| "status": "success", |
| "message": "Result updated", |
| "result": result.to_dict() |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": "Error updating result", |
| "error_details": str(e) |
| }), 500 |
|
|
| |
| |
| |
|
|
| @api.route('/statistics/summary', methods=['GET']) |
| def get_summary(): |
| """Get summary statistics""" |
| try: |
| summary = DatabaseManager.get_results_summary() |
| |
| return jsonify({ |
| "status": "success", |
| "message": "Summary retrieved", |
| "summary": summary |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": "Error getting summary", |
| "error_details": str(e) |
| }), 500 |
|
|
| @api.route('/config', methods=['GET']) |
| def get_config(): |
| """Get detector configuration""" |
| try: |
| config = { |
| "enabled_detectors": DetectorsConfig.get_enabled_detectors(), |
| "aggregation_method": DetectorsConfig.AGGREGATION_METHOD, |
| "detector_weights": DetectorsConfig.DETECTOR_WEIGHTS, |
| "detector_info": { |
| "roberta": { |
| "name": "RoBERTa Detector", |
| "description": "Fine-tuned RoBERTa model for AI text detection", |
| "model": DetectorsConfig.ROBERTA_MODEL, |
| }, |
| "perplexity": { |
| "name": "Perplexity-based Detector", |
| "description": "Detects AI patterns through perplexity and repetition analysis", |
| "model": DetectorsConfig.PERPLEXITY_MODEL, |
| }, |
| "llmdet": { |
| "name": "LLMDet Detector", |
| "description": "Combines entropy and classification metrics", |
| }, |
| "hf_classifier": { |
| "name": "HuggingFace Classifier", |
| "description": "Generic HF-based sequence classification", |
| "model": DetectorsConfig.HF_CLASSIFIER_MODEL, |
| }, |
| "outfox": { |
| "name": "OUTFOX Statistical Detector", |
| "description": "Statistical signature-based detection", |
| }, |
| } |
| } |
| |
| return jsonify({ |
| "status": "success", |
| "config": config |
| }), 200 |
| |
| except Exception as e: |
| return jsonify({ |
| "status": "error", |
| "message": "Error getting config", |
| "error_details": str(e) |
| }), 500 |
|
|