misakovhearst
Initial deploy
48c7fed
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
# Create blueprint
api = Blueprint('api', __name__, url_prefix='/api')
# Global detector ensemble
_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
# ============================================================================
# Health & Status Endpoints
# ============================================================================
@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
# ============================================================================
# Analysis Endpoints
# ============================================================================
@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')
# Validate text
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
# Clean text
text = TextCleaner.clean(text)
# Get text stats
text_stats = TextCleaner.get_text_stats(text)
# Run detection
ensemble = get_detector_ensemble()
ensemble_result = ensemble.detect(text)
# Save to database
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:
# Check if file is in request
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
# Validate file format
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
# Save uploaded file temporarily
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)
# Validate file size
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
# Parse file
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
# Validate extracted text
if not text or len(text) < 10:
os.remove(temp_filepath)
return jsonify({
"status": "error",
"message": "Could not extract sufficient text from file"
}), 400
# Clean text
text = TextCleaner.clean(text)
text_stats = TextCleaner.get_text_stats(text)
# Run detection
ensemble = get_detector_ensemble()
ensemble_result = ensemble.detect(text)
# Save to database
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,
)
# Clean up temp file
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
# ============================================================================
# Results Endpoints
# ============================================================================
@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')
# Validate pagination
if page < 1:
page = 1
if limit < 1 or limit > 100:
limit = 10
# Map sort parameter
order_by = "upload_timestamp_desc" if sort == "recent" else "score_desc"
# Calculate offset
offset = (page - 1) * limit
# Get results
results = DatabaseManager.get_all_results(
limit=limit,
offset=offset,
order_by=order_by
)
# Get total count
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
# ============================================================================
# Statistics Endpoints
# ============================================================================
@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