import gradio as gr from groq import Groq import os os.environ['GROQ_API_KEY'] = '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' 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", # Liberation Sans fonts "fontconfig", # Font configuration "fonts-dejavu-core" # DejaVu fonts as fallback ], 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 with multiple fallbacks""" standard_paths = [ "/usr/share/fonts", "/usr/local/share/fonts", "/usr/share/fonts/truetype", "~/.fonts" ] font_paths = { 'regular': None, 'bold': None } # Common font filenames to try fonts_to_try = { 'regular': [ 'LiberationSans-Regular.ttf', 'DejaVuSans.ttf', 'FreeSans.ttf' ], 'bold': [ 'LiberationSans-Bold.ttf', 'DejaVuSans-Bold.ttf', 'FreeSans-Bold.ttf' ] } def find_font(font_name: str) -> Optional[str]: """Search for a font file in standard locations""" for base_path in standard_paths: for root, _, files in os.walk(os.path.expanduser(base_path)): if font_name in files: return os.path.join(root, font_name) return None # Try to find each font for style in ['regular', 'bold']: for font_name in fonts_to_try[style]: font_path = find_font(font_name) if font_path: font_paths[style] = font_path break # If no fonts found, try using fc-match as fallback if not all(font_paths.values()): try: for style in ['regular', 'bold']: if not font_paths[style]: result = subprocess.run( ['fc-match', '-f', '%{file}', 'sans-serif:style=' + style], capture_output=True, text=True ) if result.returncode == 0 and result.stdout.strip(): font_paths[style] = result.stdout.strip() except Exception as e: print(f"Warning: Could not use fc-match to find fonts: {e}") 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.background_color = '#FFFFFF' self.border_color = '#1C1D1F' # 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: if self.font_paths['regular'] and self.font_paths['bold']: fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 36) fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 14) fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 20) fonts['name'] = ImageFont.truetype(self.font_paths['bold'], 32) else: raise ValueError("No suitable fonts found") except Exception as e: print(f"Font loading error: {e}. Using default font.") default = ImageFont.load_default() fonts = { 'title': default, 'subtitle': default, 'text': default, 'name': default } return fonts def _add_professional_border(self, draw: ImageDraw.Draw): # Single elegant inner border with padding padding = 40 draw.rectangle( [(padding, padding), (self.certificate_size[0] - padding, self.certificate_size[1] - padding)], outline='#1C1D1F', width=2 ) def _add_content( self, draw: ImageDraw.Draw, fonts: Dict[str, ImageFont.FreeTypeFont], name: str, course_name: str, score: float ): # Add "CERTIFICATE OF COMPLETION" text draw.text((60, 140), "CERTIFICATE OF COMPLETION", font=fonts['subtitle'], fill='#666666') # Add course name (large and bold) course_name = course_name.strip() or "Assessment" text_width = draw.textlength(course_name, fonts['title']) max_width = self.certificate_size[0] - 120 # Leave margins if text_width > max_width: words = course_name.split() lines = [] current_line = [] current_width = 0 for word in words: word_width = draw.textlength(word + " ", fonts['title']) if current_width + word_width <= max_width: current_line.append(word) current_width += word_width else: lines.append(" ".join(current_line)) current_line = [word] current_width = word_width if current_line: lines.append(" ".join(current_line)) course_name = "\n".join(lines) draw.multiline_text((60, 200), course_name, font=fonts['title'], fill='#1C1D1F', spacing=10) # Add instructor info draw.text((60, 300), "Instructor", font=fonts['subtitle'], fill='#666666') draw.text((60, 330), "Quiztastic", font=fonts['text'], fill='#1C1D1F') # Add participant name (large) name = name.strip() or "Participant" draw.text((60, 420), name, font=fonts['name'], fill='#1C1D1F') # Add date and score info with spacing date_str = datetime.now().strftime("%b. %d, %Y") # Date section draw.text((60, 500), "Date", font=fonts['subtitle'], fill='#666666') draw.text((60, 530), date_str, font=fonts['text'], fill='#1C1D1F') # Score section draw.text((300, 500), "Score", font=fonts['subtitle'], fill='#666666') draw.text((300, 530), f"{float(score):.1f}%", font=fonts['text'], fill='#1C1D1F') # Footer section with certificate number and reference certificate_id = f"Certificate no: {datetime.now().strftime('%Y%m%d')}-{abs(hash(name)) % 10000:04d}" ref_number = f"Reference Number: {abs(hash(name + date_str)) % 10000:04d}" # Draw footer text aligned to left and right draw.text((60, 720), certificate_id, font=fonts['subtitle'], fill='#666666') draw.text((1140, 720), ref_number, font=fonts['subtitle'], fill='#666666', anchor="ra") def _add_logo(self, certificate: Image.Image, logo_path: str): try: logo = Image.open(logo_path) # Resize logo to appropriate size logo.thumbnail((150, 80)) # Position in top-left corner with padding certificate.paste(logo, (60, 50), mask=logo if 'A' in logo.getbands() else None) except Exception as e: print(f"Error adding logo: {e}") def _add_photo(self, certificate: Image.Image, photo_path: str): try: photo = Image.open(photo_path) # Create circular mask size = (100, 100) mask = Image.new('L', size, 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0, size[0], size[1]), fill=255) # Resize photo maintaining aspect ratio photo.thumbnail(size) # Create a circular photo output = Image.new('RGBA', size, (0, 0, 0, 0)) output.paste(photo, (0, 0)) output.putalpha(mask) # Position in top-right corner with padding certificate.paste(output, (1000, 50), mask=output) except Exception as e: print(f"Error adding photo: {e}") def generate( self, score: float, name: str, course_name: str, company_logo: Optional[str] = None, participant_photo: Optional[str] = None ) -> str: try: certificate = self._create_base_certificate() draw = ImageDraw.Draw(certificate) # Add professional border self._add_professional_border(draw) fonts = self._load_fonts() self._add_content(draw, fonts, str(name), str(course_name), float(score)) if company_logo: self._add_logo(certificate, company_logo) if participant_photo: self._add_photo(certificate, 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 _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}
{symbol} Your answer: {f.selected} {'' if f.is_correct else f'
Correct answer: {f.correct_answer}'}
""" # Add result message if passed: feedback_html += self._create_success_message(score) result_msg = f"🎉 Congratulations! You passed with {score:.1f}%" else: feedback_html += self._create_failure_message(score) result_msg = f"Score: {score:.1f}%. You need 80% to pass." return ( gr.update(value=feedback_html), *[gr.update(visible=False) for _ in range(5)], score, result_msg, gr.update(selected=2) ) def _create_success_message(self, score: float) -> str: return f"""

🎉 Congratulations!

You passed the assessment with a score of {score:.1f}%

Your certificate has been generated.

""" def _create_failure_message(self, score: float) -> str: return f"""

Please Try Again

Your score: {score:.1f}%

You need 80% or higher to pass and receive a certificate.

""" def create_quiz_interface(): if not os.getenv("GROQ_API_KEY"): raise EnvironmentError("Please set your GROQ_API_KEY environment variable") quiz_app = QuizApp(os.getenv("GROQ_API_KEY")) with gr.Blocks(title="Quiztastic AI", theme=gr.themes.Soft()) as demo: # State management current_questions = gr.State([]) current_question_idx = gr.State(0) answer_state = gr.State([None] * 5) # Store all answers # Header gr.Markdown(""" # 🎓 Quiztastic AI ### Transforming content into Quizzes. """) with gr.Tabs() as tabs: # Profile Setup Tab with gr.Tab(id=1,label="📋 Step 1: Profile Setup"): with gr.Row(): name = gr.Textbox(label="Full Name", placeholder="Enter your full name") email = gr.Textbox(label="Email", placeholder="Enter your email") text_input = gr.Textbox( label="Learning Content", placeholder="Enter the text content you want to be assessed on", lines=10 ) num_questions = gr.Slider( minimum=1, maximum=20, value=10, step=1, label="Number of Questions" ) with gr.Row(): company_logo = gr.Image(label="Company Logo (Optional)", type="filepath") participant_photo = gr.Image(label="Your Photo (Optional)", type="filepath") generate_btn = gr.Button("Generate Assessment", variant="primary", size="lg") # Assessment Tab with gr.Tab(id=2,label="📝 Step 2: Take Assessment") as assessment_tab: with gr.Column(visible=True) as question_box: # Question section with gr.Group(): # Question display question_display = gr.Markdown("", label="Current Question") # Single radio group for current question current_options = gr.Radio( choices=[], label="Select your answer:", visible=False ) # Navigation with gr.Row(): prev_btn = gr.Button("← Previous", variant="secondary", size="sm") question_counter = gr.Markdown("Question 1") next_btn = gr.Button("Next →", variant="secondary", size="sm") gr.Markdown("---") # Separator with gr.Row(): submit_btn = gr.Button( "Submit Assessment", variant="primary", size="lg" ) reset_btn = gr.Button( "Reset Quiz", variant="secondary", size="lg" ) # Results section with gr.Group(visible=False) as results_group: feedback_box = gr.Markdown("") with gr.Row(): view_cert_btn = gr.Button( "View Certificate", variant="primary", size="lg", visible=False ) back_to_assessment = gr.Button( # Add this button definition "Back to Assessment", variant="secondary", size="lg", visible=True ) # Certification Tab with gr.Tab(id=3,label="🎓 Step 3: Get Certified"): score_display = gr.Number(label="Your Score") result_message = gr.Markdown("") course_name = gr.Textbox( label="Certification Title", value="Professional Assessment Certification" ) certificate_display = gr.Image(label="Your Certificate") # Helper Functions def on_generate_questions(text, num_questions): """Generate quiz questions and setup initial state""" success, questions = quiz_app.generate_questions(text, num_questions) if not success: return [ "", # question_display gr.update(choices=[], visible=False), # current_options "", # question_counter gr.update(visible=False), # question_box [], # current_questions 0, # current_question_idx [None] * 5, # answer_state gr.Tabs(selected=2), # tabs gr.update(visible=False), # results_group gr.update(visible=False) # view_cert_btn - added this ] # Setup initial state with first question initial_answers = [None] * len(questions) question = questions[0] question_html = f"""## Question 1 {question.question} Please select one answer:""" return [ question_html, # question_display gr.update( choices=question.options, value=None, visible=True, label="Select your answer for Question 1:" ), # current_options f"Question 1 of {len(questions)}", # question_counter gr.update(visible=True), # question_box questions, # current_questions 0, # current_question_idx initial_answers, # answer_state gr.Tabs(selected=2), # tabs gr.update(visible=False), # results_group gr.update(visible=False) # view_cert_btn - added this ] def navigate(direction, current_idx, questions, answers, current_answer): """Handle navigation between questions""" if not questions: return [ 0, answers, "", gr.update(choices=[], value=None, visible=False), "", gr.update(visible=False) ] new_answers = list(answers) if current_answer and 0 <= current_idx < len(new_answers): new_answers[current_idx] = current_answer new_idx = max(0, min(len(questions) - 1, current_idx + direction)) question = questions[new_idx] return [ new_idx, new_answers, f"## Question {new_idx + 1}\n{question.question}\n\nPlease select one answer:", gr.update( choices=question.options, value=new_answers[new_idx] if new_idx < len(new_answers) else None, visible=True, label=f"Select your answer for Question {new_idx + 1}:" ), f"Question {new_idx + 1} of {len(questions)}", gr.update(visible=True) ] def handle_prev(current_idx, questions, answers, current_answer): return navigate(-1, current_idx, questions, answers, current_answer) def handle_next(current_idx, questions, answers, current_answer): return navigate(1, current_idx, questions, answers, current_answer) def update_answer_state(answer, idx, current_answers): new_answers = list(current_answers) if 0 <= idx < len(new_answers): new_answers[idx] = answer return new_answers def reset_quiz(text, num_questions): """Handle quiz reset""" return on_generate_questions(text, num_questions) def view_certificate(): """Navigate to certificate tab""" return gr.Tabs(selected=2) def on_submit(questions, answers, current_idx, current_answer): final_answers = list(answers) if 0 <= current_idx < len(final_answers): final_answers[current_idx] = current_answer if not all(a is not None for a in final_answers[:len(questions)]): return [ "⚠️ Please answer all questions before submitting.", gr.update(visible=True), 0, "", gr.update(visible=True), gr.Tabs(selected=2), gr.update(visible=False) ] score, passed, feedback = quiz_app.calculate_score(final_answers[:len(questions)]) feedback_html = "# Assessment Results\n\n" for i, (q, f) in enumerate(zip(questions, feedback)): color = "green" if f.is_correct else "red" symbol = "✅" if f.is_correct else "❌" feedback_html += f"""### Question {i+1} {q.question}
{symbol} Your answer: {f.selected or "No answer"} {'' if f.is_correct else f'
Correct answer: {f.correct_answer}'}
\n""" result_msg = "🎉 Passed!" if passed else "Please try again" if not passed: feedback_html += f"""

Please Try Again

Your score: {score:.1f}%

You need 80% or higher to pass and receive a certificate.

""" else: feedback_html += f"""

🎉 Congratulations!

You passed with a score of {score:.1f}%

Click "View Certificate" to see your certificate.

""" return [ feedback_html, gr.update(visible=True), score, result_msg, gr.update(visible=False), gr.Tabs(selected=2), gr.update(visible=passed) ] # Event handlers generate_btn.click(fn=on_generate_questions, inputs=[text_input, num_questions], outputs=[question_display, current_options, question_counter, question_box, current_questions, current_question_idx, answer_state, tabs, results_group, view_cert_btn]).then(fn=lambda: gr.Tabs(selected=2), outputs=tabs) prev_btn.click(fn=handle_prev, inputs=[current_question_idx, current_questions, answer_state, current_options], outputs=[current_question_idx, answer_state, question_display, current_options, question_counter, question_box]) next_btn.click(fn=handle_next, inputs=[current_question_idx, current_questions, answer_state, current_options], outputs=[current_question_idx, answer_state, question_display, current_options, question_counter, question_box]) current_options.change(fn=update_answer_state, inputs=[current_options, current_question_idx, answer_state], outputs=answer_state) submit_btn.click(fn=on_submit, inputs=[current_questions, answer_state, current_question_idx, current_options], outputs=[feedback_box, results_group, score_display, result_message, question_box, tabs, view_cert_btn]) reset_btn.click(fn=reset_quiz, inputs=[text_input, num_questions], outputs=[question_display, current_options, question_counter, question_box, current_questions, current_question_idx, answer_state, tabs, results_group, view_cert_btn]) view_cert_btn.click(fn=lambda: gr.Tabs(selected=3), outputs=tabs) back_to_assessment.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs) score_display.change(fn=lambda s, n, c, l, p: quiz_app.certificate_generator.generate(s, n, c, l, p) or gr.update(value=None), inputs=[score_display, name, course_name, company_logo, participant_photo], outputs=certificate_display) return demo if __name__ == "__main__": demo = create_quiz_interface() demo.launch()