Essay-grader-ai / modules /report_generator.py
Turbiling's picture
Update modules/report_generator.py
bb56246 verified
Raw
History Blame Contribute Delete
15.9 kB
# modules/report_generator.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from datetime import datetime
import logging
import os
from typing import Dict, Any
import arabic_reshaper
from bidi.algorithm import get_display
logger = logging.getLogger(__name__)
class PDFReportGenerator:
def __init__(self):
self.primary_color = HexColor('#1E40AF')
self.success_color = HexColor('#059669')
self.warning_color = HexColor('#F59E0B')
self.danger_color = HexColor('#DC2626')
self.gray_color = HexColor('#6B7280')
try:
font_path = self._download_urdu_font()
if font_path:
pdfmetrics.registerFont(TTFont('UrduFont', font_path))
self.urdu_font_available = True
logger.info("Urdu font registered successfully")
else:
self.urdu_font_available = False
logger.warning("Urdu font not available")
except Exception as e:
logger.error(f"Font registration failed: {e}")
self.urdu_font_available = False
logger.info("PDFReportGenerator initialized")
def _download_urdu_font(self):
"""Download Urdu font if not exists"""
try:
import requests
font_dir = 'assets/fonts'
os.makedirs(font_dir, exist_ok=True)
font_path = os.path.join(font_dir, 'NotoNastaliqUrdu-Regular.ttf')
if not os.path.exists(font_path):
logger.info("Downloading Urdu font...")
url = "https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoNastaliqUrdu/NotoNastaliqUrdu-Regular.ttf"
response = requests.get(url, timeout=30)
if response.status_code == 200:
with open(font_path, 'wb') as f:
f.write(response.content)
logger.info("Urdu font downloaded successfully")
return font_path
else:
return font_path
except Exception as e:
logger.error(f"Font download failed: {e}")
return None
def _format_urdu_text(self, text):
"""Format Urdu text for proper display"""
if not self.urdu_font_available:
return text
try:
reshaped_text = arabic_reshaper.reshape(text)
bidi_text = get_display(reshaped_text)
return bidi_text
except Exception as e:
logger.error(f"Urdu text formatting failed: {e}")
return text
def create_report(self, student_name: str, grade_level: int, essay_type: str, grading_result: Dict[str, Any], output_path: str) -> str:
try:
logger.info(f"Creating PDF report: {output_path}")
c = canvas.Canvas(output_path, pagesize=A4)
width, height = A4
self._create_cover_page(c, width, height, student_name, grade_level, essay_type, grading_result)
c.showPage()
self._create_scores_page(c, width, height, grading_result)
c.showPage()
self._create_feedback_pages(c, width, height, grading_result)
c.save()
logger.info("PDF report created successfully")
return output_path
except Exception as e:
logger.exception("Failed to create PDF")
raise
def _create_cover_page(self, c, width, height, student_name, grade_level, essay_type, result):
c.setFillColor(self.primary_color)
c.rect(0, height - 80, width, 80, fill=1, stroke=0)
c.setFillColorRGB(1, 1, 1)
c.setFont('Helvetica-Bold', 28)
c.drawCentredString(width/2, height - 50, "Essay Grading Report")
c.setFont('Helvetica', 12)
today = datetime.now().strftime("%B %d, %Y")
c.drawCentredString(width/2, height - 70, today)
c.setFillColorRGB(0, 0, 0)
y = height - 150
c.setFont('Helvetica-Bold', 16)
c.drawString(100, y, "Student Details:")
c.setFont('Helvetica', 14)
c.drawString(100, y - 30, f"Name: {student_name}")
c.drawString(100, y - 55, f"Grade: {grade_level}")
c.drawString(100, y - 80, f"Essay Type: {essay_type}")
score = result['total_score']
grade = result['grade']
if score >= 80:
color = self.success_color
elif score >= 60:
color = self.warning_color
else:
color = self.danger_color
c.setFillColor(color)
c.setFont('Helvetica-Bold', 72)
c.drawCentredString(width/2, height/2 + 20, f"{int(score)}")
c.setFont('Helvetica', 24)
c.setFillColor(self.gray_color)
c.drawCentredString(width/2, height/2 - 25, "/ 100")
c.setFont('Helvetica-Bold', 48)
c.setFillColor(color)
c.drawCentredString(width/2, height/2 - 90, f"Grade: {grade}")
c.setFillColor(self.gray_color)
c.setFont('Helvetica-Oblique', 10)
c.drawCentredString(width/2, 50, "Generated by Essay Grader AI | Powered by Groq")
c.setFont('Helvetica-Oblique', 9)
c.drawCentredString(width/2, 35, "Developed by: Najaf Ali Sharqi")
def _create_scores_page(self, c, width, height, result):
c.setFillColor(self.primary_color)
c.setFont('Helvetica-Bold', 24)
c.drawString(50, height - 60, "Detailed Score Breakdown")
try:
chart_path = self._create_bar_chart(result['scores'])
if os.path.exists(chart_path):
c.drawImage(chart_path, 50, height - 400, width=500, height=300, preserveAspectRatio=True)
os.remove(chart_path)
except Exception as e:
logger.error(f"Chart creation failed: {e}")
y = height - 450
c.setFont('Helvetica-Bold', 14)
c.setFillColorRGB(0, 0, 0)
c.drawString(50, y, "Category")
c.drawString(250, y, "Score")
c.drawString(350, y, "Max")
c.drawString(450, y, "Percentage")
c.setStrokeColor(self.primary_color)
c.setLineWidth(2)
c.line(50, y - 5, width - 50, y - 5)
categories = {
'content': ('Content & Arguments', 40),
'organization': ('Organization', 20),
'language': ('Language & Style', 20),
'grammar': ('Grammar & Spelling', 20)
}
c.setFont('Helvetica', 12)
y -= 30
for key, (name, max_score) in categories.items():
score = result['scores'].get(key, 0)
percentage = (score / max_score) * 100 if max_score > 0 else 0
if percentage >= 80:
color = self.success_color
elif percentage >= 60:
color = self.warning_color
else:
color = self.danger_color
c.setFillColorRGB(0, 0, 0)
c.drawString(50, y, name)
c.setFillColor(color)
c.setFont('Helvetica-Bold', 12)
c.drawString(250, y, str(int(score)))
c.setFillColorRGB(0, 0, 0)
c.setFont('Helvetica', 12)
c.drawString(350, y, str(max_score))
c.drawString(450, y, f"{percentage:.1f}%")
y -= 30
y -= 20
c.setFillColor(HexColor('#F3F4F6'))
c.roundRect(50, y - 80, width - 100, 70, 10, fill=1)
c.setFillColor(self.primary_color)
c.setFont('Helvetica-Bold', 16)
c.drawString(70, y - 30, "Overall Performance:")
c.setFont('Helvetica', 14)
total = result['total_score']
if total >= 80:
performance = "Excellent Work!"
elif total >= 60:
performance = "Good Job!"
elif total >= 40:
performance = "Fair - Keep Improving"
else:
performance = "Needs Significant Improvement"
c.drawString(70, y - 55, f"• {performance}")
def _create_feedback_pages(self, c, width, height, result):
y = height - 60
c.setFillColor(self.primary_color)
c.setFont('Helvetica-Bold', 24)
c.drawString(50, y, "Detailed Feedback")
y -= 40
# Strengths
c.setFillColor(self.success_color)
c.setFont('Helvetica-Bold', 16)
c.drawString(50, y, "Strengths")
y -= 25
c.setFillColorRGB(0, 0, 0)
strengths = result.get('strengths', [])
if strengths:
for i, strength in enumerate(strengths[:5], 1):
if y < 100:
c.showPage()
y = height - 60
c.setFillColor(self.success_color)
c.circle(60, y + 3, 4, fill=1)
c.setFillColorRGB(0, 0, 0)
if self.urdu_font_available and self._contains_urdu(strength):
c.setFont('UrduFont', 11)
formatted_text = self._format_urdu_text(f"{i}. {strength}")
self._draw_wrapped_text_rtl(c, formatted_text, 75, y, width - 140, 'UrduFont', 11)
else:
c.setFont('Helvetica', 11)
text = f"{i}. {strength}"
self._draw_wrapped_text(c, text, 75, y, width - 140, 'Helvetica', 11)
y -= 25
y -= 20
# Areas for Improvement
if y < 150:
c.showPage()
y = height - 60
c.setFillColor(self.warning_color)
c.setFont('Helvetica-Bold', 16)
c.drawString(50, y, "Areas for Improvement")
y -= 25
weaknesses = result.get('weaknesses', [])
if weaknesses:
for weakness in weaknesses[:5]:
if y < 150:
c.showPage()
y = height - 60
c.setFillColor(self.warning_color)
c.circle(60, y + 3, 4, fill=1)
issue = weakness.get('issue', '')
c.setFillColorRGB(0, 0, 0)
if self.urdu_font_available and self._contains_urdu(issue):
c.setFont('UrduFont', 11)
formatted_issue = self._format_urdu_text(issue)
self._draw_wrapped_text_rtl(c, formatted_issue, 75, y, width - 140, 'UrduFont', 11)
else:
c.setFont('Helvetica-Bold', 11)
self._draw_wrapped_text(c, issue, 75, y, width - 140, 'Helvetica-Bold', 11)
y -= 20
if 'suggestion' in weakness and weakness['suggestion']:
suggestion = weakness['suggestion']
c.setFillColor(self.success_color)
if self.urdu_font_available and self._contains_urdu(suggestion):
c.setFont('UrduFont', 10)
formatted_sugg = self._format_urdu_text(f"→ {suggestion}")
self._draw_wrapped_text_rtl(c, formatted_sugg, 75, y, width - 140, 'UrduFont', 10)
else:
c.setFont('Helvetica', 10)
self._draw_wrapped_text(c, f"→ {suggestion}", 75, y, width - 140, 'Helvetica', 10)
y -= 20
y -= 10
# Overall Feedback
if y < 200:
c.showPage()
y = height - 60
y -= 20
c.setFillColor(self.primary_color)
c.setFont('Helvetica-Bold', 16)
c.drawString(50, y, "Overall Feedback")
y -= 25
c.setFillColorRGB(0, 0, 0)
feedback = result.get('overall_feedback', 'Great effort!')
if self.urdu_font_available and self._contains_urdu(feedback):
c.setFont('UrduFont', 11)
formatted_feedback = self._format_urdu_text(feedback)
self._draw_wrapped_text_rtl(c, formatted_feedback, 60, y, width - 120, 'UrduFont', 11)
else:
c.setFont('Helvetica', 11)
self._draw_wrapped_text(c, feedback, 60, y, width - 120, 'Helvetica', 11)
def _contains_urdu(self, text):
"""Check if text contains Urdu characters"""
if not text:
return False
return any('\u0600' <= char <= '\u06FF' for char in text)
def _draw_wrapped_text(self, c, text, x, y, max_width, font, size):
"""Draw text with word wrapping (LTR)"""
words = text.split()
line = ""
for word in words:
test_line = line + word + " "
if c.stringWidth(test_line, font, size) < max_width:
line = test_line
else:
c.drawString(x, y, line.strip())
y -= size + 3
line = word + " "
if line.strip():
c.drawString(x, y, line.strip())
def _draw_wrapped_text_rtl(self, c, text, x, y, max_width, font, size):
"""Draw text with word wrapping (RTL for Urdu)"""
words = text.split()
line = ""
for word in words:
test_line = word + " " + line
if c.stringWidth(test_line, font, size) < max_width:
line = test_line
else:
c.drawRightString(x + max_width, y, line.strip())
y -= size + 3
line = word + " "
if line.strip():
c.drawRightString(x + max_width, y, line.strip())
def _create_bar_chart(self, scores: Dict[str, int]) -> str:
fig, ax = plt.subplots(figsize=(10, 6))
categories = {
'content': ('Content', 40),
'organization': ('Organization', 20),
'language': ('Language', 20),
'grammar': ('Grammar', 20)
}
names = [v[0] for v in categories.values()]
obtained = [scores.get(k, 0) for k in categories.keys()]
maximum = [v[1] for v in categories.values()]
x = range(len(names))
width_bar = 0.35
bars1 = ax.bar([i - width_bar/2 for i in x], obtained, width_bar, label='Obtained', color='#1E40AF')
bars2 = ax.bar([i + width_bar/2 for i in x], maximum, width_bar, label='Maximum', color='#E5E7EB', alpha=0.5)
ax.set_xlabel('Category', fontsize=12, fontweight='bold')
ax.set_ylabel('Score', fontsize=12, fontweight='bold')
ax.set_title('Score Breakdown', fontsize=14, fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels(names)
ax.legend()
ax.grid(axis='y', alpha=0.3)
for bar in bars1:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height, f'{int(height)}', ha='center', va='bottom', fontweight='bold')
plt.tight_layout()
chart_path = 'outputs/temp_chart.png'
plt.savefig(chart_path, format='png', dpi=150, bbox_inches='tight')
plt.close()
return chart_path