import gradio as gr from groq import Groq import os from PIL import Image, ImageDraw, ImageFont from datetime import datetime import json import tempfile from typing import List, Dict, Tuple, Optional from dataclasses import dataclass import subprocess @dataclass class Question: question: str options: List[str] correct_answer: int @dataclass class QuizFeedback: is_correct: bool selected: Optional[str] correct_answer: str class QuizGenerator: def __init__(self, api_key: str): self.client = Groq(api_key=api_key) def generate_questions(self, text: str, num_questions: int) -> List[Question]: prompt = self._create_prompt(text, num_questions) try: response = self.client.chat.completions.create( messages=[ { "role": "system", "content": "You are a quiz generator. Create clear questions with concise answer options." }, { "role": "user", "content": prompt } ], model="llama-3.2-3b-preview", temperature=0.3, max_tokens=2048 ) questions = self._parse_response(response.choices[0].message.content) return self._validate_questions(questions, num_questions) except Exception as e: raise QuizGenerationError(f"Failed to generate questions: {str(e)}") def _create_prompt(self, text: str, num_questions: int) -> str: return f"""Create exactly {num_questions} multiple choice questions based on this text: {text} For each question: 1. Create a clear, concise question 2. Provide exactly 4 options 3. Mark the correct answer with the index (0-3) 4. Ensure options are concise and clear Return ONLY a JSON array with this EXACT format - no other text: [ {{ "question": "Question text here?", "options": [ "Brief option 1", "Brief option 2", "Brief option 3", "Brief option 4" ], "correct_answer": 0 }} ] Keep all options concise (10 words or less each). """ def _parse_response(self, response_text: str) -> List[Dict]: response_text = response_text.replace("```json", "").replace("```", "").strip() start_idx = response_text.find("[") end_idx = response_text.rfind("]") if start_idx == -1 or end_idx == -1: raise ValueError("No valid JSON array found in response") response_text = response_text[start_idx:end_idx + 1] return json.loads(response_text) def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]: validated = [] for q in questions: if not self._is_valid_question(q): continue validated.append(Question( question=q["question"].strip(), options=[opt.strip()[:100] for opt in q["options"]], correct_answer=int(q["correct_answer"]) % 4 )) if not validated: raise ValueError("No valid questions after validation") return validated[:num_questions] def _is_valid_question(self, question: Dict) -> bool: return ( all(key in question for key in ["question", "options", "correct_answer"]) and isinstance(question["options"], list) and len(question["options"]) == 4 and all(isinstance(opt, str) for opt in question["options"]) ) class FontManager: """Manages font installation and loading for the certificate generator""" @staticmethod def install_fonts(): """Install required fonts if they're not already present""" try: # Install fonts package subprocess.run([ "apt-get", "update", "-y" ], check=True) subprocess.run([ "apt-get", "install", "-y", "fonts-liberation", # Provides Liberation fonts (Arial alternative) "fontconfig" # Font configuration ], check=True) # Clear font cache subprocess.run(["fc-cache", "-f"], check=True) print("Fonts installed successfully") except subprocess.CalledProcessError as e: print(f"Warning: Could not install fonts: {e}") except Exception as e: print(f"Warning: Unexpected error installing fonts: {e}") @staticmethod def get_font_paths() -> Dict[str, str]: """Get the paths to the required fonts""" # Liberation Sans is a metric-compatible replacement for Arial font_paths = { 'regular': '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', 'bold': '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf' } # Fallback paths for different distributions fallback_paths = { 'regular': [ '/usr/share/fonts/liberation-sans/LiberationSans-Regular.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', '/usr/share/fonts/TTF/LiberationSans-Regular.ttf' ], 'bold': [ '/usr/share/fonts/liberation-sans/LiberationSans-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', '/usr/share/fonts/TTF/LiberationSans-Bold.ttf' ] } # Check fallback paths for style in ['regular', 'bold']: if not os.path.exists(font_paths[style]): for path in fallback_paths[style]: if os.path.exists(path): font_paths[style] = path break return font_paths class QuizGenerationError(Exception): """Exception raised for errors in quiz generation""" pass class CertificateGenerator: def __init__(self): self.certificate_size = (1200, 800) self.border_color = '#4682B4' self.background_color = '#F0F8FF' # Install fonts if needed FontManager.install_fonts() self.font_paths = FontManager.get_font_paths() def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: """Load fonts with fallbacks""" fonts = {} try: # Use Liberation Sans fonts instead of Arial fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 60) fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 40) fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 30) except Exception as e: print(f"Font loading error: {e}. Using default font.") default = ImageFont.load_default() fonts = { 'title': default, 'text': default, 'subtitle': default } return fonts def generate( self, score: float, name: str, course_name: str, company_logo: Optional[str] = None, participant_photo: Optional[str] = None ) -> str: """ Generate a certificate with custom styling and optional logo/photo """ try: certificate = self._create_base_certificate() draw = ImageDraw.Draw(certificate) fonts = self._load_fonts() self._add_borders(draw) self._add_content(draw, fonts, str(name), str(course_name), float(score)) self._add_images(certificate, company_logo, participant_photo) return self._save_certificate(certificate) except Exception as e: print(f"Error generating certificate: {e}") return None def _create_base_certificate(self) -> Image.Image: return Image.new('RGB', self.certificate_size, self.background_color) def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: try: return { 'title': ImageFont.truetype("arial.ttf", 60), 'text': ImageFont.truetype("arial.ttf", 40), 'subtitle': ImageFont.truetype("arial.ttf", 30) } except Exception as e: print(f"Font loading error: {e}. Using default font.") default = ImageFont.load_default() return {'title': default, 'text': default, 'subtitle': default} def _add_borders(self, draw: ImageDraw.Draw): # Main borders draw.rectangle([20, 20, 1180, 780], outline=self.border_color, width=3) draw.rectangle([40, 40, 1160, 760], outline=self.border_color, width=1) # Decorative corners self._add_decorative_corners(draw) def _add_decorative_corners(self, draw: ImageDraw.Draw): corner_size = 20 corners = [ # Top-left [(20, 40), (20 + corner_size, 40)], [(40, 20), (40, 20 + corner_size)], # Top-right [(1180 - corner_size, 40), (1180, 40)], [(1160, 20), (1160, 20 + corner_size)], # Bottom-left [(20, 760), (20 + corner_size, 760)], [(40, 780 - corner_size), (40, 780)], # Bottom-right [(1180 - corner_size, 760), (1180, 760)], [(1160, 780 - corner_size), (1160, 780)] ] for corner in corners: draw.line(corner, fill=self.border_color, width=2) def _add_content( self, draw: ImageDraw.Draw, fonts: Dict[str, ImageFont.FreeTypeFont], name: str, course_name: str, score: float ): # Title and headers draw.text((600, 100), "CertifyMe AI", font=fonts['title'], fill=self.border_color, anchor="mm") draw.text((600, 160), "Certificate of Achievement", font=fonts['subtitle'], fill=self.border_color, anchor="mm") # Clean inputs name = str(name).strip() or "Participant" course_name = str(course_name).strip() or "Assessment" # Main content content = [ (300, "This is to certify that", 'black'), (380, name, self.border_color), (460, "has successfully completed", 'black'), (540, course_name, self.border_color), (620, f"with a score of {float(score):.1f}%", 'black'), (700, datetime.now().strftime("%B %d, %Y"), 'black') ] for y, text, color in content: draw.text((600, y), text, font=fonts['text'], fill=color, anchor="mm") def _add_images( self, certificate: Image.Image, company_logo: Optional[str], participant_photo: Optional[str] ): if company_logo: self._add_image(certificate, company_logo, (50, 50)) if participant_photo: self._add_image(certificate, participant_photo, (1000, 50)) def _add_image(self, certificate: Image.Image, image_path: str, position: Tuple[int, int]): try: img = Image.open(image_path) img.thumbnail((150, 150)) certificate.paste(img, position) except Exception as e: print(f"Error adding image: {e}") def _save_certificate(self, certificate: Image.Image) -> str: temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png') certificate.save(temp_file.name, 'PNG', quality=95) return temp_file.name class QuizApp: def __init__(self, api_key: str): self.quiz_generator = QuizGenerator(api_key) self.certificate_generator = CertificateGenerator() self.current_questions: List[Question] = [] def generate_questions(self, text: str, num_questions: int) -> Tuple[bool, List[Question]]: """ Generate quiz questions using the QuizGenerator Returns (success, questions) tuple """ try: questions = self.quiz_generator.generate_questions(text, num_questions) self.current_questions = questions return True, questions except Exception as e: print(f"Error generating questions: {e}") return False, [] def calculate_score(self, answers: List[Optional[str]]) -> Tuple[float, bool, List[QuizFeedback]]: """ Calculate the quiz score and generate feedback Returns (score, passed, feedback) tuple """ if not answers or not self.current_questions: return 0, False, [] feedback = [] correct = 0 for question, answer in zip(self.current_questions, answers): if answer is None: feedback.append(QuizFeedback(False, None, question.options[question.correct_answer])) continue try: selected_index = question.options.index(answer) is_correct = selected_index == question.correct_answer if is_correct: correct += 1 feedback.append(QuizFeedback( is_correct, answer, question.options[question.correct_answer] )) except ValueError: feedback.append(QuizFeedback(False, answer, question.options[question.correct_answer])) score = (correct / len(self.current_questions)) * 100 return score, score >= 80, feedback def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]: """ Event handler for generating new questions """ if not text.strip(): return ( gr.update(value=""), gr.update(value="⚠️ Please enter some text content to generate questions."), *[gr.update(visible=False, choices=[]) for _ in range(5)], [], gr.update(selected=1) ) success, questions = self.generate_questions(text, num_questions) if not success or not questions: return ( gr.update(value=""), gr.update(value="❌ Failed to generate questions. Please try again."), *[gr.update(visible=False, choices=[]) for _ in range(5)], [], gr.update(selected=1) ) # Create question display questions_html = "# 📝 Assessment Questions\n\n" questions_html += "> Please select one answer for each question.\n\n" # Update radio buttons updates = [] for i, q in enumerate(questions): questions_html += f"### Question {i+1}\n{q.question}\n\n" updates.append(gr.update( visible=True, choices=q.options, value=None, label=f"Select your answer:" )) # Hide unused radio buttons for i in range(len(questions), 5): updates.append(gr.update(visible=False, choices=[])) return ( gr.update(value=questions_html), gr.update(value=""), *updates, questions, gr.update(selected=1) ) def submit_quiz(self, q1: Optional[str], q2: Optional[str], q3: Optional[str], q4: Optional[str], q5: Optional[str], questions: List[Question] ) -> Tuple[gr.update, List[gr.update], float, str, gr.update]: """ Event handler for quiz submission """ answers = [q1, q2, q3, q4, q5][:len(questions)] if not all(a is not None for a in answers): return ( gr.update(value="⚠️ Please answer all questions before submitting."), *[gr.update() for _ in range(5)], 0, "", gr.update(selected=1) ) score, passed, feedback = self.calculate_score(answers) # Create feedback HTML feedback_html = "# Assessment Results\n\n" for i, (q, f) in enumerate(zip(self.current_questions, feedback)): color = "green" if f.is_correct else "red" symbol = "✅" if f.is_correct else "❌" feedback_html += f""" ### Question {i+1} {q.question}
You passed the assessment with a score of {score:.1f}%
Your certificate has been generated.
Your score: {score:.1f}%
You need 80% or higher to pass and receive a certificate.