Spaces:
Build error
Build error
| # 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 |