import logging import os import sys import asyncio from concurrent.futures import ThreadPoolExecutor from datetime import datetime # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) logger.info("Starting Hugging Face Spaces application...") logger.info(f"Python version: {sys.version}") try: logger.info("Importing FastAPI...") from fastapi import FastAPI, File, UploadFile, Form, Request, HTTPException logger.info("Importing CORSMiddleware...") from fastapi.middleware.cors import CORSMiddleware logger.info("Importing JSONResponse, FileResponse...") from fastapi.responses import JSONResponse, FileResponse, HTMLResponse logger.info("Importing StaticFiles...") from fastapi.staticfiles import StaticFiles logger.info("Importing json...") import json logger.info("Importing OCR...") from OCR import OCR logger.info("Importing Grader...") from Feedback import Grader logger.info("Importing PDFFeedbackGenerator...") from PDFFeedbackGenerator import PDFFeedbackGenerator logger.info("Importing pandas...") import pandas as pd logger.info("Importing BytesIO...") from io import BytesIO logger.info("Importing tempfile...") import tempfile logger.info("Importing shutil...") import shutil logger.info("Importing typing...") from typing import List, Dict, Any logger.info("Importing pdf2image...") from pdf2image import convert_from_path logger.info("Importing platform...") import platform logger.info("Importing cv2...") import cv2 logger.info("All imports successful.") except ImportError as e: logger.error(f"Failed to import a required module: {e}") logger.error("Please ensure all dependencies in 'requirements.txt' are installed.") sys.exit(1) # Exit if imports fail app = FastAPI( title="CSS Essay Grader API - Hugging Face Spaces", description="API for processing and grading essays with OCR and AI feedback - Deployed on Hugging Face Spaces", version="1.0.0", docs_url="/docs", redoc_url="/redoc" ) # Enable CORS for all routes app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Constants LOGO_PATH = "cslogo.png" TEMP_DIR = "temp" OUTPUT_DIR = "output" # Create necessary directories os.makedirs(TEMP_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True) # Initialize instances api_key = os.environ.get("OPENAI_API_KEY") if not api_key: raise RuntimeError("OPENAI_API_KEY environment variable not set") ocr = OCR() # Initialize enhanced Grader with production configuration grader_config = { 'enable_validation': True, 'enable_enhanced_logging': True, 'fallback_to_legacy': True, 'aggregate_scores': True, 'log_missing_categories': True } grader = Grader(api_key=api_key, config=grader_config) pdf_generator = PDFFeedbackGenerator(output_path=os.path.join(OUTPUT_DIR, "feedback.pdf"), logo_path=LOGO_PATH) # Create a thread pool executor for handling long-running tasks executor = ThreadPoolExecutor(max_workers=4) def preprocess_essay_text(text: str) -> str: """ Preprocess essay text to remove problematic characters and normalize formatting. """ import unicodedata # Remove control characters except newlines and tabs text = ''.join(char for char in text if unicodedata.category(char)[0] != 'C' or char in '\n\r\t') # Normalize Unicode characters text = unicodedata.normalize('NFKC', text) # Replace problematic characters text = text.replace('\u201c', '"').replace('\u201d', '"') # Smart quotes text = text.replace('\u2018', "'").replace('\u2019', "'") # Smart single quotes text = text.replace('\u2013', '-').replace('\u2014', '-') # En/em dashes text = text.replace('\u2022', '•') # Bullet points text = text.replace('\u00a0', ' ') # Non-breaking spaces text = text.replace('\u2026', '...') # Ellipsis text = text.replace('\u2014', '--') # Em dash text = text.replace('\u2013', '-') # En dash # Clean up multiple spaces and newlines import re text = re.sub(r'\s+', ' ', text) # Replace multiple whitespace with single space text = re.sub(r'\n\s*\n', '\n\n', text) # Clean up multiple newlines return text.strip() def process_pdf_with_poppler(file_path: str) -> tuple: """Process PDF file with optimized Poppler configuration.""" try: # Use system-installed Poppler (much faster and smaller) # Convert PDF to images with optimized settings images = convert_from_path( file_path, dpi=200, # Reduced from 300 for better performance thread_count=1, # Reduced for container environments grayscale=True, # Smaller file size size=(1654, 2340) # A4 size at 200 DPI ) # Save the first page as a temporary image temp_image_path = os.path.join(TEMP_DIR, f"temp_{os.path.basename(file_path)}.png") images[0].save(temp_image_path, "PNG", optimize=True, quality=85) try: # Process with OCR using the converted image extracted_text, accuracy_metrics = ocr.process_image_with_vision(temp_image_path) return extracted_text, accuracy_metrics finally: # Clean up temporary image if os.path.exists(temp_image_path): os.remove(temp_image_path) except Exception as e: logger.error(f"Error processing PDF {file_path}: {str(e)}") raise Exception(f"Error processing PDF: {str(e)}") @app.get("/", response_class=HTMLResponse) def root(): """Root endpoint with HTML welcome page for Hugging Face Spaces.""" html_content = """ CSS Essay Grader API - Hugging Face Spaces

🎓 CSS Essay Grader API

Welcome to the CSS Essay Grader API deployed on Hugging Face Spaces! This API provides comprehensive essay analysis, OCR text extraction, and AI-powered feedback.

Available Endpoints:

GET
/health
Health check endpoint
POST
/api/upload
Upload and process a single file (image or PDF)
POST
/api/upload/bulk
Upload and process multiple files (images or PDFs)
POST
/api/essay-analysis
Generate comprehensive essay analysis with AI feedback
POST
/api/feedback
Generate feedback for essay text
POST
/api/grammar-analysis
NEW: Generate grammar and punctuation analysis only (line-by-line processing)
POST
/api/essay-analysis-with-question
NEW: Generate comprehensive essay analysis based on a specific question (essay_text + question)
POST
/api/feedback-with-question
NEW: Generate feedback for essay text based on a specific question (essay_text + question)
POST
/api/verify
Verify and analyze text quality

🚀 Enhanced Features Now Available:

""" return HTMLResponse(content=html_content) @app.get("/health") def health_check(): """Health check endpoint.""" try: # Check if all components are working status = { "status": "healthy", "service": "CSS Essay Grader API - Hugging Face Spaces", "components": { "ocr": "initialized", "grader": "initialized", "pdf_generator": "initialized" }, "timestamp": str(datetime.now()), "version": "1.0.0", "deployment": "huggingface-spaces" } return status except Exception as e: return { "status": "unhealthy", "service": "CSS Essay Grader API - Hugging Face Spaces", "error": str(e), "timestamp": str(datetime.now()) } @app.post('/api/upload') async def upload_file(file: UploadFile = File(...)): """Upload and process a single file (image or PDF).""" try: # Save uploaded file to temp directory file_path = os.path.join(TEMP_DIR, file.filename) with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) try: # Process file based on type if file.filename.lower().endswith(".pdf"): extracted_text, accuracy_metrics = ocr.process_pdf_file_with_vision(file_path) else: extracted_text, accuracy_metrics = ocr.process_image_with_vision(file_path) # Get accuracy status and analysis status, message = ocr.accuracy_analyzer.get_accuracy_status(accuracy_metrics) analysis_points = ocr.accuracy_analyzer.get_detailed_analysis(accuracy_metrics) word_count = len(extracted_text.split()) response = { "success": True, "extracted_text": extracted_text, "filename": file.filename, "word_count": word_count, "ocr_quality": { "status": status, "message": message, "analysis_points": analysis_points, "metrics": accuracy_metrics } } return response finally: # Clean up temp file if os.path.exists(file_path): os.remove(file_path) except Exception as e: logger.error(f"Error processing file: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @app.post('/api/upload/bulk') async def upload_bulk_files( files: List[UploadFile] = File(...) ): """Upload and process multiple files (images or PDFs).""" if len(files) > 10: # Reduced from 15 for better performance raise HTTPException(status_code=400, detail="You can upload a maximum of 10 files at once.") results = [] extracted_texts = [] for file in files: try: # Save uploaded file to temp directory file_path = os.path.join(TEMP_DIR, file.filename) with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) # Process file based on type if file.filename.lower().endswith(".pdf"): try: extracted_text, accuracy_metrics = ocr.process_pdf_file_with_vision(file_path) except Exception as pdf_error: logger.error(f"Error processing PDF {file.filename}: {str(pdf_error)}") results.append({ "filename": file.filename, "error": str(pdf_error) }) continue else: extracted_text, accuracy_metrics = ocr.process_image_with_vision(file_path) # Check OCR accuracy - Updated threshold to 20% if accuracy_metrics.get("overall_accuracy", 0.0) < 0.2: results.append({ "filename": file.filename, "error": "OCR accuracy is below 20%. Please upload a clearer image or higher quality file.", "accuracy": accuracy_metrics.get("overall_accuracy", 0.0) }) continue if not extracted_text.strip(): results.append({ "filename": file.filename, "error": "No text extracted from file", "accuracy": accuracy_metrics.get("overall_accuracy", 0.0) }) continue # Get accuracy status and analysis status, message = ocr.accuracy_analyzer.get_accuracy_status(accuracy_metrics) analysis_points = ocr.accuracy_analyzer.get_detailed_analysis(accuracy_metrics) word_count = len(extracted_text.split()) extracted_texts.append(extracted_text) results.append({ "filename": file.filename, "extracted_text": extracted_text, "word_count": word_count, "ocr_quality": { "status": status, "message": message, "analysis_points": analysis_points, "metrics": accuracy_metrics } }) # Clean up temp file os.remove(file_path) except Exception as e: logger.error(f"Error processing file {file.filename}: {str(e)}") results.append({ "filename": file.filename, "error": str(e) }) # Combine all extracted texts combined_text = "\n\n".join(extracted_texts) if extracted_texts else "" # Return only results and combined_text, no feedback return { "results": results, "combined_text": combined_text } @app.post('/api/verify') async def verify_text(text: str = Form(...)): """Verify and analyze text quality.""" try: # Simple text analysis word_count = len(text.split()) char_count = len(text) return { "word_count": word_count, "char_count": char_count, "text_length": "short" if word_count < 100 else "medium" if word_count < 500 else "long" } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post('/api/feedback') async def get_feedback( essay_text: str = Form(...), question: str = Form(None) ): """Generate feedback for essay text. Optionally provide a question for question-specific feedback. Now also returns line-by-line feedback.""" try: if not essay_text.strip(): raise HTTPException(status_code=400, detail="Essay text cannot be empty") # Preprocess the essay text to clean problematic characters essay_text = preprocess_essay_text(essay_text) # Enhanced logging: Check essay length and processing method essay_length = len(essay_text) word_count = len(essay_text.split()) token_count = grader.count_tokens(essay_text) logger.info(f"Processing feedback request: {word_count} words, {token_count} tokens, {essay_length} characters - FULL TEXT") # Always process full text without any chunking or truncation # Check if question is provided for question-specific feedback if question and question.strip(): # Generate question-specific feedback feedback = grader.grade_answer_with_question( essay_text, question.strip() ) feedback_type = "question_specific" else: # Generate general feedback feedback = grader.grade_answer_with_gpt( essay_text, "Provide comprehensive feedback on this essay including grammar, structure, and content analysis." ) feedback_type = "general" # Enhanced logging: Full text processing logger.info("Essay processed using full text method - NO TRUNCATION") # --- NEW: Add line-by-line feedback --- # Remove all unlimited_analysis and line_feedback logic from feedback endpoints # Only return overall_feedback in /api/feedback overall_feedback = feedback # Return both overall and line-by-line feedback return { "feedback_type": feedback_type, "overall_feedback": overall_feedback, "evaluationAndScoring": overall_feedback.get("evaluationAndScoring", []), "essayStructure": overall_feedback.get("essayStructure", []), "issues_summary": { "total_issues": overall_feedback.get("total_issues_found", 0), "vocabulary_issues": overall_feedback.get("vocabulary_issues", []), "grammar_issues": overall_feedback.get("grammar_issues", []), "issues_by_category": { section["label"]: { "count": section.get("issuesCount", 0), "issues": section.get("issuesList", []) } for section in overall_feedback.get("evaluationAndScoring", []) } }, "processing_info": { "word_count": word_count, "token_count": token_count, "chunked_processing": False, # No chunking in this endpoint "chunks_used": 1 # Always 1 chunk for full text } } except Exception as e: logger.error(f"Error in get_feedback: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to generate feedback: {str(e)}") @app.post('/api/grammar-analysis') async def get_grammar_analysis( essay_text: str = Form(...) ): """Generate grammar and punctuation analysis only for essay text. Returns a single section with all issues aggregated.""" try: if not essay_text.strip(): raise HTTPException(status_code=400, detail="Essay text cannot be empty") essay_text = preprocess_essay_text(essay_text) essay_length = len(essay_text) word_count = len(essay_text.split()) token_count = grader.count_tokens(essay_text) logger.info(f"Processing grammar analysis request: {word_count} words, {token_count} tokens, {essay_length} characters - FULL TEXT") text_length = len(essay_text) logger.info(f"Processing full essay text: {text_length} characters - NO TRUNCATION") grammar_analysis = grader.analyze_grammar_only(essay_text) line_by_line_grammar = grammar_analysis.get('line_by_line_grammar', []) # Aggregate issues and positive points all_issues = [] all_positive_points = [] all_scores = [] for line in line_by_line_grammar: all_issues.extend(line.get('grammar_issues', [])) all_positive_points.extend(line.get('positive_points', [])) score = line.get('grammar_score') if isinstance(score, (int, float)): all_scores.append(score) overall_score = int(sum(all_scores) / len(all_scores)) if all_scores else 0 analysis = grammar_analysis.get('overall_grammar_summary', {}).get('analysis', 'Grammar & Punctuation analysis completed.') section = { "name": "Grammar & Punctuation", "score": overall_score, "analysis": analysis, "issues": all_issues, "positive_points": list(set(all_positive_points)), "issues_count": len(all_issues) } return {"feedback": {"sections": [section]}} except Exception as e: logger.error(f"Error in grammar analysis: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to generate grammar analysis: {str(e)}") @app.post('/api/essay-analysis') async def get_essay_analysis( essay_text: str = Form(...), question: str = Form(None) ): """Generate comprehensive essay analysis with enhanced mandatory feedback for each topic/question. Optionally provide a question for question-specific analysis.""" try: if not essay_text.strip(): raise HTTPException(status_code=400, detail="Essay text cannot be empty") # Preprocess the essay text to clean problematic characters essay_text = preprocess_essay_text(essay_text) # Enhanced logging: Check essay length and processing method essay_length = len(essay_text) word_count = len(essay_text.split()) token_count = grader.count_tokens(essay_text) logger.info(f"Processing essay analysis request: {word_count} words, {token_count} tokens, {essay_length} characters - FULL TEXT") # Always process full text without any chunking or truncation text_length = len(essay_text) logger.info(f"Processing full essay text: {text_length} characters - NO TRUNCATION") # Get original essay word count original_essay_word_count = len(essay_text.split()) # Use thread pool executor for long-running tasks with timeout loop = asyncio.get_event_loop() # Generate rewritten essay with better error handling and timeout try: rephrased_analysis = await loop.run_in_executor( executor, lambda: grader.rephrase_text_with_gpt(essay_text) ) rewritten_essay = rephrased_analysis.get('rephrased_text', essay_text) except Exception as rephrase_error: logger.error(f"Error in rephrasing: {str(rephrase_error)}") # Fallback to original text if rephrasing fails rewritten_essay = essay_text rewritten_essay_word_count = len(rewritten_essay.split()) # Check if question is provided for question-specific analysis if question and question.strip(): # Generate enhanced evaluation feedback with mandatory question-specific analysis try: feedback = await loop.run_in_executor( executor, lambda: grader.grade_answer_with_question( essay_text, question.strip() ) ) analysis_type = "question_specific" except Exception as feedback_error: logger.error(f"Error in generating question-specific feedback: {str(feedback_error)}") # Create a comprehensive fallback feedback structure feedback = { "sections": [ { "name": "Grammar & Punctuation", "score": 70, "analysis": "Basic grammar analysis completed", "issues": [], "positive_points": ["Essay demonstrates basic grammar understanding"], "issues_count": 0 }, { "name": "Vocabulary Usage", "score": 75, "analysis": "Vocabulary analysis completed", "issues": [], "positive_points": ["Appropriate vocabulary usage"], "issues_count": 0 }, { "name": "Sentence Structure", "score": 80, "analysis": "Sentence structure analysis completed", "issues": [], "positive_points": ["Good sentence variety"], "issues_count": 0 }, { "name": "Content Relevance & Depth", "score": 75, "analysis": "Content relevance analysis completed", "issues": [], "positive_points": ["Content addresses the topic"], "issues_count": 0 }, { "name": "Argument Development", "score": 70, "analysis": "Argument development analysis completed", "issues": [], "positive_points": ["Arguments are presented"], "issues_count": 0 }, { "name": "Evidence & Citations", "score": 65, "analysis": "Evidence and citations analysis completed", "issues": [], "positive_points": ["Some evidence provided"], "issues_count": 0 }, { "name": "Structure & Organization", "score": 75, "analysis": "Structure and organization analysis completed", "issues": [], "positive_points": ["Essay has clear structure"], "issues_count": 0 }, { "name": "Conclusion Quality", "score": 70, "analysis": "Conclusion quality analysis completed", "issues": [], "positive_points": ["Conclusion is present"], "issues_count": 0 } ], "overall_score": 72, "essay_structure": { "Introduction & Thesis": { "Clear Thesis Statement": {"value": True, "explanation": "Thesis statement analysis completed", "suggestions": "Consider strengthening the thesis"}, "Engaging Introduction": {"value": True, "explanation": "Introduction analysis completed", "suggestions": "Make introduction more engaging"}, "Background Context": {"value": True, "explanation": "Background context analysis completed", "suggestions": "Provide more background context"} }, "Body Development": { "Topic Sentences": {"value": True, "explanation": "Topic sentences analysis completed", "suggestions": "Strengthen topic sentences"}, "Supporting Evidence": {"value": True, "explanation": "Supporting evidence analysis completed", "suggestions": "Add more supporting evidence"}, "Logical Flow": {"value": True, "explanation": "Logical flow analysis completed", "suggestions": "Improve logical flow"}, "Paragraph Coherence": {"value": True, "explanation": "Paragraph coherence analysis completed", "suggestions": "Enhance paragraph coherence"} }, "Content Quality": { "Relevance to Topic": {"value": True, "explanation": "Topic relevance analysis completed", "suggestions": "Ensure all content is relevant"}, "Depth of Analysis": {"value": True, "explanation": "Analysis depth completed", "suggestions": "Deepen the analysis"}, "Use of Examples": {"value": True, "explanation": "Examples analysis completed", "suggestions": "Include more specific examples"}, "Critical Thinking": {"value": True, "explanation": "Critical thinking analysis completed", "suggestions": "Demonstrate more critical thinking"} }, "Evidence & Citations": { "Factual Accuracy": {"value": True, "explanation": "Factual accuracy analysis completed", "suggestions": "Verify all facts"}, "Source Credibility": {"value": True, "explanation": "Source credibility analysis completed", "suggestions": "Use more credible sources"}, "Proper Citations": {"value": True, "explanation": "Citations analysis completed", "suggestions": "Improve citation format"}, "Statistical Data": {"value": True, "explanation": "Statistical data analysis completed", "suggestions": "Include more statistical data"} }, "Conclusion": { "Summary of Arguments": {"value": True, "explanation": "Argument summary analysis completed", "suggestions": "Strengthen argument summary"}, "Policy Recommendations": {"value": True, "explanation": "Policy recommendations analysis completed", "suggestions": "Provide specific policy recommendations"}, "Future Implications": {"value": True, "explanation": "Future implications analysis completed", "suggestions": "Discuss future implications"}, "Strong Closing": {"value": True, "explanation": "Closing analysis completed", "suggestions": "Create a stronger closing"} } }, "question_specific_feedback": { "question": question.strip(), "question_relevance_score": 70, "question_coverage": "Question coverage analysis completed", "covered_aspects": ["Essay addresses the main question"], "missing_aspects": ["Consider addressing additional aspects of the question"], "strengths": ["Essay addresses the main question"], "improvement_suggestions": ["Provide more comprehensive question coverage"] } } else: # Generate enhanced evaluation feedback with mandatory topic-specific analysis try: feedback = await loop.run_in_executor( executor, lambda: grader.grade_answer_with_gpt( essay_text, "Provide comprehensive mandatory feedback on this essay including grammar, structure, content analysis, and topic-specific evaluation." ) ) analysis_type = "general" except Exception as feedback_error: logger.error(f"Error in generating feedback: {str(feedback_error)}") # Create a comprehensive fallback feedback structure feedback = { "sections": [ { "name": "Grammar & Punctuation", "score": 70, "analysis": "Basic grammar analysis completed", "issues": [], "positive_points": ["Essay demonstrates basic grammar understanding"], "issues_count": 0 }, { "name": "Vocabulary Usage", "score": 75, "analysis": "Vocabulary analysis completed", "issues": [], "positive_points": ["Appropriate vocabulary usage"], "issues_count": 0 }, { "name": "Sentence Structure", "score": 80, "analysis": "Sentence structure analysis completed", "issues": [], "positive_points": ["Good sentence variety"], "issues_count": 0 }, { "name": "Content Relevance & Depth", "score": 75, "analysis": "Content relevance analysis completed", "issues": [], "positive_points": ["Content addresses the topic"], "issues_count": 0 }, { "name": "Argument Development", "score": 70, "analysis": "Argument development analysis completed", "issues": [], "positive_points": ["Arguments are presented"], "issues_count": 0 }, { "name": "Evidence & Citations", "score": 65, "analysis": "Evidence and citations analysis completed", "issues": [], "positive_points": ["Some evidence provided"], "issues_count": 0 }, { "name": "Structure & Organization", "score": 75, "analysis": "Structure and organization analysis completed", "issues": [], "positive_points": ["Essay has clear structure"], "issues_count": 0 }, { "name": "Conclusion Quality", "score": 70, "analysis": "Conclusion quality analysis completed", "issues": [], "positive_points": ["Conclusion is present"], "issues_count": 0 } ], "overall_score": 72, "essay_structure": { "Introduction & Thesis": { "Clear Thesis Statement": {"value": True, "explanation": "Thesis statement analysis completed", "suggestions": "Consider strengthening the thesis"}, "Engaging Introduction": {"value": True, "explanation": "Introduction analysis completed", "suggestions": "Make introduction more engaging"}, "Background Context": {"value": True, "explanation": "Background context analysis completed", "suggestions": "Provide more background context"} }, "Body Development": { "Topic Sentences": {"value": True, "explanation": "Topic sentences analysis completed", "suggestions": "Strengthen topic sentences"}, "Supporting Evidence": {"value": True, "explanation": "Supporting evidence analysis completed", "suggestions": "Add more supporting evidence"}, "Logical Flow": {"value": True, "explanation": "Logical flow analysis completed", "suggestions": "Improve logical flow"}, "Paragraph Coherence": {"value": True, "explanation": "Paragraph coherence analysis completed", "suggestions": "Enhance paragraph coherence"} }, "Content Quality": { "Relevance to Topic": {"value": True, "explanation": "Topic relevance analysis completed", "suggestions": "Ensure all content is relevant"}, "Depth of Analysis": {"value": True, "explanation": "Analysis depth completed", "suggestions": "Deepen the analysis"}, "Use of Examples": {"value": True, "explanation": "Examples analysis completed", "suggestions": "Include more specific examples"}, "Critical Thinking": {"value": True, "explanation": "Critical thinking analysis completed", "suggestions": "Demonstrate more critical thinking"} }, "Evidence & Citations": { "Factual Accuracy": {"value": True, "explanation": "Factual accuracy analysis completed", "suggestions": "Verify all facts"}, "Source Credibility": {"value": True, "explanation": "Source credibility analysis completed", "suggestions": "Use more credible sources"}, "Proper Citations": {"value": True, "explanation": "Citations analysis completed", "suggestions": "Improve citation format"}, "Statistical Data": {"value": True, "explanation": "Statistical data analysis completed", "suggestions": "Include more statistical data"} }, "Conclusion": { "Summary of Arguments": {"value": True, "explanation": "Argument summary analysis completed", "suggestions": "Strengthen argument summary"}, "Policy Recommendations": {"value": True, "explanation": "Policy recommendations analysis completed", "suggestions": "Provide specific policy recommendations"}, "Future Implications": {"value": True, "explanation": "Future implications analysis completed", "suggestions": "Discuss future implications"}, "Strong Closing": {"value": True, "explanation": "Closing analysis completed", "suggestions": "Create a stronger closing"} } }, "topic_specific_feedback": { "topic_coverage": "Topic coverage analysis completed", "missing_aspects": ["Consider addressing additional aspects of the topic"], "strengths": ["Essay addresses the main topic"], "improvement_suggestions": ["Provide more comprehensive topic coverage"] } } # Enhanced logging: Check if chunking was used # No chunking in this endpoint # Extract overall score overall_score = feedback.get("overall_score", 0) # Transform enhanced evaluation sections to match required format evaluation_and_scoring = [] for section in feedback.get("sections", []): section_name = section.get("name", "") score = section.get("score", 0) analysis = section.get("analysis", "") issues = section.get("issues", []) positive_points = section.get("positive_points", []) issues_count = section.get("issues_count", 0) # Transform issues to match required format issues_list = [] for issue in issues: issues_list.append({ "before": issue.get("before", ""), "after": issue.get("after", ""), "explanation": issue.get("explanation", "") }) evaluation_and_scoring.append({ "label": section_name, "score": score, "analysis": analysis, "issuesCount": issues_count, "issuesList": issues_list, "positivePoints": positive_points }) # Transform enhanced essay structure to match required format essay_structure = [] original_essay_structure = feedback.get('essay_structure', {}) # Introduction & Thesis section intro_features = [] if 'Introduction & Thesis' in original_essay_structure: intro_data = original_essay_structure['Introduction & Thesis'] for key, value in intro_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" intro_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Introduction & Thesis", "features": intro_features }) # Body Development section body_features = [] if 'Body Development' in original_essay_structure: body_data = original_essay_structure['Body Development'] for key, value in body_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" body_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Body Development", "features": body_features }) # Content Quality section content_features = [] if 'Content Quality' in original_essay_structure: content_data = original_essay_structure['Content Quality'] for key, value in content_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" content_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Content Quality", "features": content_features }) # Evidence & Citations section evidence_features = [] if 'Evidence & Citations' in original_essay_structure: evidence_data = original_essay_structure['Evidence & Citations'] for key, value in evidence_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" evidence_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Evidence & Citations", "features": evidence_features }) # Conclusion section conclusion_features = [] if 'Conclusion' in original_essay_structure: conclusion_data = original_essay_structure['Conclusion'] for key, value in conclusion_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" conclusion_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Conclusion", "features": conclusion_features }) # Get question-specific feedback or topic-specific feedback if question and question.strip(): question_feedback = feedback.get('question_specific_feedback', {}) response_data = { "originalEssayWordCount": original_essay_word_count, "reWrittenEssayWordCount": rewritten_essay_word_count, "originalEssay": essay_text, "reWrittenEssay": rewritten_essay, "evaluationAndScoring": feedback.get("evaluationAndScoring", evaluation_and_scoring), "essayStructure": feedback.get("essayStructure", essay_structure), "question": question.strip(), "questionSpecificFeedback": question_feedback, "analysisType": analysis_type, "issuesSummary": { "totalIssues": feedback.get("total_issues_found", 0), "vocabularyIssues": feedback.get("vocabulary_issues", []), "grammarIssues": feedback.get("grammar_issues", []), "issuesByCategory": { section["label"]: { "count": section.get("issuesCount", 0), "issues": section.get("issuesList", []) } for section in feedback.get("evaluationAndScoring", []) } } } else: topic_feedback = feedback.get('topic_specific_feedback', {}) response_data = { "originalEssayWordCount": original_essay_word_count, "reWrittenEssayWordCount": rewritten_essay_word_count, "originalEssay": essay_text, "reWrittenEssay": rewritten_essay, "evaluationAndScoring": feedback.get("evaluationAndScoring", evaluation_and_scoring), "essayStructure": feedback.get("essayStructure", essay_structure), "topicSpecificFeedback": { "topicCoverage": topic_feedback.get('topic_coverage', 'Topic coverage analysis completed'), "missingAspects": topic_feedback.get('missing_aspects', ['Consider additional aspects']), "strengths": topic_feedback.get('strengths', ['Essay addresses the topic']), "improvementSuggestions": topic_feedback.get('improvement_suggestions', ['Provide more comprehensive coverage']) }, "analysisType": analysis_type, "issuesSummary": { "totalIssues": feedback.get("total_issues_found", 0), "vocabularyIssues": feedback.get("vocabulary_issues", []), "grammarIssues": feedback.get("grammar_issues", []), "issuesByCategory": { section["label"]: { "count": section.get("issuesCount", 0), "issues": section.get("issuesList", []) } for section in feedback.get("evaluationAndScoring", []) } } } return response_data except asyncio.TimeoutError: logger.error("Essay analysis timed out") raise HTTPException(status_code=408, detail="Analysis timed out. Please try with a shorter essay.") except Exception as e: logger.error(f"Error generating essay analysis: {str(e)}") # Provide a more informative error message error_detail = str(e) if "Invalid control character" in error_detail: error_detail = "The essay text contains invalid characters that cannot be processed. Please check for special characters or formatting issues." elif "JSON" in error_detail: error_detail = "There was an issue processing the essay analysis. Please try with a shorter or simpler text." elif "timeout" in error_detail.lower(): error_detail = "The analysis took too long to complete. Please try with a shorter essay." raise HTTPException(status_code=500, detail=error_detail) @app.get('/api/download-pdf/{pdf_path:path}') async def download_pdf(pdf_path: str): """Download generated PDF file.""" try: full_path = os.path.join(OUTPUT_DIR, pdf_path) if not os.path.exists(full_path): raise HTTPException(status_code=404, detail="PDF file not found") return FileResponse( full_path, media_type='application/pdf', filename=os.path.basename(pdf_path) ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # Hugging Face Spaces specific endpoint @app.get("/spaces-info") def spaces_info(): """Information about the Hugging Face Spaces deployment.""" return { "space_name": "CSS Essay Grader API", "deployment": "huggingface-spaces", "port": 7860, "framework": "fastapi", "features": [ "OCR Text Extraction", "Essay Analysis", "AI-Powered Feedback", "PDF Processing", "Bulk File Upload" ], "documentation": "/docs", "health_check": "/health" } @app.post('/api/feedback-with-question') async def get_feedback_with_question( essay_text: str = Form(...), question: str = Form(...) ): """Generate feedback for essay text based on a specific question.""" try: if not essay_text.strip(): raise HTTPException(status_code=400, detail="Essay text cannot be empty") if not question.strip(): raise HTTPException(status_code=400, detail="Question cannot be empty") # Preprocess the essay text to clean problematic characters essay_text = preprocess_essay_text(essay_text) # Generate question-specific feedback feedback = grader.grade_answer_with_question( essay_text, question ) # Generate PDF if requested try: pdf_path = pdf_generator.create_feedback_pdf( "Student", f"Essay Analysis - Question: {question}", feedback ) return { "feedback": feedback, "question": question, "pdf_path": pdf_path } except Exception as pdf_error: logger.error(f"PDF generation failed: {str(pdf_error)}") return { "feedback": feedback, "question": question, "pdf_error": str(pdf_error) } except Exception as e: logger.error(f"Error generating feedback with question: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @app.post('/api/essay-analysis-with-question') async def get_essay_analysis_with_question( essay_text: str = Form(...), question: str = Form(...) ): """Generate comprehensive essay analysis with enhanced mandatory feedback for a specific question.""" try: if not essay_text.strip(): raise HTTPException(status_code=400, detail="Essay text cannot be empty") if not question.strip(): raise HTTPException(status_code=400, detail="Question cannot be empty") # Preprocess the essay text to clean problematic characters essay_text = preprocess_essay_text(essay_text) # Process full text without any truncation text_length = len(essay_text) logger.info(f"Processing full essay text: {text_length} characters - NO TRUNCATION") # Get original essay word count original_essay_word_count = len(essay_text.split()) # Use thread pool executor for long-running tasks with timeout loop = asyncio.get_event_loop() # Generate rewritten essay with better error handling and timeout try: rephrased_analysis = await loop.run_in_executor( executor, lambda: grader.rephrase_text_with_gpt(essay_text) ) rewritten_essay = rephrased_analysis.get('rephrased_text', essay_text) except Exception as rephrase_error: logger.error(f"Error in rephrasing: {str(rephrase_error)}") # Fallback to original text if rephrasing fails rewritten_essay = essay_text rewritten_essay_word_count = len(rewritten_essay.split()) # Generate enhanced evaluation feedback with mandatory question-specific analysis try: feedback = await loop.run_in_executor( executor, lambda: grader.grade_answer_with_question( essay_text, question ) ) except Exception as feedback_error: logger.error(f"Error in generating feedback: {str(feedback_error)}") # Create a comprehensive fallback feedback structure feedback = { "sections": [ { "name": "Grammar & Punctuation", "score": 70, "analysis": "Basic grammar analysis completed", "issues": [], "positive_points": ["Essay demonstrates basic grammar understanding"], "issues_count": 0 }, { "name": "Vocabulary Usage", "score": 75, "analysis": "Vocabulary analysis completed", "issues": [], "positive_points": ["Appropriate vocabulary usage"], "issues_count": 0 }, { "name": "Sentence Structure", "score": 80, "analysis": "Sentence structure analysis completed", "issues": [], "positive_points": ["Good sentence variety"], "issues_count": 0 }, { "name": "Content Relevance & Depth", "score": 75, "analysis": "Content relevance analysis completed", "issues": [], "positive_points": ["Content addresses the topic"], "issues_count": 0 }, { "name": "Argument Development", "score": 70, "analysis": "Argument development analysis completed", "issues": [], "positive_points": ["Arguments are presented"], "issues_count": 0 }, { "name": "Evidence & Citations", "score": 65, "analysis": "Evidence and citations analysis completed", "issues": [], "positive_points": ["Some evidence provided"], "issues_count": 0 }, { "name": "Structure & Organization", "score": 75, "analysis": "Structure and organization analysis completed", "issues": [], "positive_points": ["Essay has clear structure"], "issues_count": 0 }, { "name": "Conclusion Quality", "score": 70, "analysis": "Conclusion quality analysis completed", "issues": [], "positive_points": ["Conclusion is present"], "issues_count": 0 } ], "overall_score": 72, "essay_structure": { "Introduction & Thesis": { "Clear Thesis Statement": {"value": True, "explanation": "Thesis statement analysis completed", "suggestions": "Consider strengthening the thesis"}, "Engaging Introduction": {"value": True, "explanation": "Introduction analysis completed", "suggestions": "Make introduction more engaging"}, "Background Context": {"value": True, "explanation": "Background context analysis completed", "suggestions": "Provide more background context"} }, "Body Development": { "Topic Sentences": {"value": True, "explanation": "Topic sentences analysis completed", "suggestions": "Strengthen topic sentences"}, "Supporting Evidence": {"value": True, "explanation": "Supporting evidence analysis completed", "suggestions": "Add more supporting evidence"}, "Logical Flow": {"value": True, "explanation": "Logical flow analysis completed", "suggestions": "Improve logical flow"}, "Paragraph Coherence": {"value": True, "explanation": "Paragraph coherence analysis completed", "suggestions": "Enhance paragraph coherence"} }, "Content Quality": { "Relevance to Topic": {"value": True, "explanation": "Topic relevance analysis completed", "suggestions": "Ensure all content is relevant"}, "Depth of Analysis": {"value": True, "explanation": "Analysis depth completed", "suggestions": "Deepen the analysis"}, "Use of Examples": {"value": True, "explanation": "Examples analysis completed", "suggestions": "Include more specific examples"}, "Critical Thinking": {"value": True, "explanation": "Critical thinking analysis completed", "suggestions": "Demonstrate more critical thinking"} }, "Evidence & Citations": { "Factual Accuracy": {"value": True, "explanation": "Factual accuracy analysis completed", "suggestions": "Verify all facts"}, "Source Credibility": {"value": True, "explanation": "Source credibility analysis completed", "suggestions": "Use more credible sources"}, "Proper Citations": {"value": True, "explanation": "Citations analysis completed", "suggestions": "Improve citation format"}, "Statistical Data": {"value": True, "explanation": "Statistical data analysis completed", "suggestions": "Include more statistical data"} }, "Conclusion": { "Summary of Arguments": {"value": True, "explanation": "Argument summary analysis completed", "suggestions": "Strengthen argument summary"}, "Policy Recommendations": {"value": True, "explanation": "Policy recommendations analysis completed", "suggestions": "Provide specific policy recommendations"}, "Future Implications": {"value": True, "explanation": "Future implications analysis completed", "suggestions": "Discuss future implications"}, "Strong Closing": {"value": True, "explanation": "Closing analysis completed", "suggestions": "Create a stronger closing"} } }, "question_specific_feedback": { "question": question, "question_relevance_score": 70, "question_coverage": "Question coverage analysis completed", "covered_aspects": ["Essay addresses the main question"], "missing_aspects": ["Consider addressing additional aspects of the question"], "strengths": ["Essay addresses the main question"], "improvement_suggestions": ["Provide more comprehensive question coverage"] } } # Extract overall score overall_score = feedback.get("overall_score", 0) # Transform enhanced evaluation sections to match required format evaluation_and_scoring = [] for section in feedback.get("sections", []): section_name = section.get("name", "") score = section.get("score", 0) analysis = section.get("analysis", "") issues = section.get("issues", []) positive_points = section.get("positive_points", []) issues_count = section.get("issues_count", 0) # Transform issues to match required format issues_list = [] for issue in issues: issues_list.append({ "before": issue.get("before", ""), "after": issue.get("after", ""), "explanation": issue.get("explanation", "") }) evaluation_and_scoring.append({ "label": section_name, "score": score, "analysis": analysis, "issuesCount": issues_count, "issuesList": issues_list, "positivePoints": positive_points }) # Transform enhanced essay structure to match required format essay_structure = [] original_essay_structure = feedback.get('essay_structure', {}) # Introduction & Thesis section intro_features = [] if 'Introduction & Thesis' in original_essay_structure: intro_data = original_essay_structure['Introduction & Thesis'] for key, value in intro_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" intro_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Introduction & Thesis", "features": intro_features }) # Body Development section body_features = [] if 'Body Development' in original_essay_structure: body_data = original_essay_structure['Body Development'] for key, value in body_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" body_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Body Development", "features": body_features }) # Content Quality section content_features = [] if 'Content Quality' in original_essay_structure: content_data = original_essay_structure['Content Quality'] for key, value in content_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" content_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Content Quality", "features": content_features }) # Evidence & Citations section evidence_features = [] if 'Evidence & Citations' in original_essay_structure: evidence_data = original_essay_structure['Evidence & Citations'] for key, value in evidence_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" evidence_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Evidence & Citations", "features": evidence_features }) # Conclusion section conclusion_features = [] if 'Conclusion' in original_essay_structure: conclusion_data = original_essay_structure['Conclusion'] for key, value in conclusion_data.items(): is_correct = value.get('value', True) explanation = value.get('explanation', '') suggestions = value.get('suggestions', '') error_message = f"{explanation} {suggestions}".strip() if not is_correct else "" conclusion_features.append({ "label": key, "isCorrect": is_correct, "errorMessage": error_message if not is_correct else None }) essay_structure.append({ "label": "Conclusion", "features": conclusion_features }) # Get question-specific feedback question_feedback = feedback.get('question_specific_feedback', {}) # Return the response in the exact format required by the API documentation response_data = { "originalEssayWordCount": original_essay_word_count, "reWrittenEssayWordCount": rewritten_essay_word_count, "originalEssay": essay_text, "reWrittenEssay": rewritten_essay, "evaluationAndScoring": feedback.get("evaluationAndScoring", evaluation_and_scoring), "essayStructure": feedback.get("essayStructure", essay_structure), "question": question, "questionSpecificFeedback": question_feedback } return response_data except Exception as e: logger.error(f"Error in essay analysis with question: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": import uvicorn # For Hugging Face Spaces, we need to use port 7860 uvicorn.run(app, host="0.0.0.0", port=7860)