Spaces:
Sleeping
Sleeping
| 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 | |
| class Question: | |
| question: str | |
| options: List[str] | |
| correct_answer: int | |
| class QuizFeedback: | |
| is_correct: bool | |
| selected: Optional[str] | |
| correct_answer: str | |
| class QuizGenerationError(Exception): | |
| """Exception raised for errors in quiz generation""" | |
| pass | |
| 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: | |
| def install_fonts(): | |
| try: | |
| subprocess.run(["apt-get", "update", "-y"], check=True) | |
| subprocess.run( | |
| [ | |
| "apt-get", "install", "-y", | |
| "fonts-liberation", | |
| "fontconfig", | |
| "fonts-dejavu-core" | |
| ], | |
| check=True | |
| ) | |
| subprocess.run(["fc-cache", "-f"], check=True) | |
| print("Fonts installed successfully") | |
| except Exception as e: | |
| print(f"Warning: Font installation error: {e}") | |
| def get_font_paths() -> Dict[str, str]: | |
| standard_paths = [ | |
| "/usr/share/fonts", | |
| "/usr/local/share/fonts", | |
| "/usr/share/fonts/truetype", | |
| "~/.fonts" | |
| ] | |
| font_paths = { | |
| 'regular': None, | |
| 'bold': None | |
| } | |
| 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]: | |
| 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 | |
| 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 not all(font_paths.values()): | |
| try: | |
| for style in ['regular', 'bold']: | |
| if not font_paths[style]: | |
| result = subprocess.run( | |
| ['fc-match', '-f', '%{file}', f'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: Font matching error: {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.border_color = '#4682B4' | |
| self.background_color = '#F0F8FF' | |
| FontManager.install_fonts() | |
| self.font_paths = FontManager.get_font_paths() | |
| 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) | |
| 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 _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: | |
| fonts = {} | |
| try: | |
| if self.font_paths['regular'] and self.font_paths['bold']: | |
| 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) | |
| 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, | |
| 'text': default, | |
| 'subtitle': default | |
| } | |
| return fonts | |
| def _create_base_certificate(self) -> Image.Image: | |
| return Image.new('RGB', self.certificate_size, self.background_color) | |
| def _add_borders(self, draw: ImageDraw.Draw): | |
| draw.rectangle([20, 20, 1180, 780], outline=self.border_color, width=3) | |
| draw.rectangle([40, 40, 1160, 760], outline=self.border_color, width=1) | |
| self._add_decorative_corners(draw) | |
| def _add_decorative_corners(self, draw: ImageDraw.Draw): | |
| corner_size = 20 | |
| corners = [ | |
| [(20, 40), (20 + corner_size, 40)], | |
| [(40, 20), (40, 20 + corner_size)], | |
| [(1180 - corner_size, 40), (1180, 40)], | |
| [(1160, 20), (1160, 20 + corner_size)], | |
| [(20, 760), (20 + corner_size, 760)], | |
| [(40, 780 - corner_size), (40, 780)], | |
| [(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 | |
| ): | |
| 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") | |
| name = str(name).strip() or "Participant" | |
| course_name = str(course_name).strip() or "Assessment" | |
| 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} | |
| <div style="color: {color}; padding: 10px; margin: 5px 0; border-left: 3px solid {color};"> | |
| {symbol} Your answer: {f.selected} | |
| {'' if f.is_correct else f'<br>Correct answer: {f.correct_answer}'} | |
| </div> | |
| """ | |
| # 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""" | |
| <div style="background-color: #e6ffe6; padding: 20px; margin-top: 20px; border-radius: 10px;"> | |
| <h3 style="color: #008000;">🎉 Congratulations!</h3> | |
| <p>You passed the assessment with a score of {score:.1f}%</p> | |
| <p>Your certificate has been generated.</p> | |
| </div> | |
| """ | |
| def _create_failure_message(self, score: float) -> str: | |
| return f""" | |
| <div style="background-color: #ffe6e6; padding: 20px; margin-top: 20px; border-radius: 10px;"> | |
| <h3 style="color: #cc0000;">Please Try Again</h3> | |
| <p>Your score: {score:.1f}%</p> | |
| <p>You need 80% or higher to pass and receive a certificate.</p> | |
| </div> | |
| """ | |
| def create_quiz_interface(): | |
| if not os.getenv("GROQ_API_KEY"): | |
| raise EnvironmentError("Please set your GROQ_API_KEY environment variable") | |
| global quiz_app | |
| quiz_app = QuizApp(os.getenv("GROQ_API_KEY")) | |
| with gr.Blocks(title="CertifyMe 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) | |
| gr.Markdown(""" | |
| # 🎓 CertifyMe AI | |
| ### Transform Your Knowledge into Recognized Achievements | |
| """) | |
| with gr.Tabs() as tabs: | |
| # Profile Setup Tab | |
| with gr.Tab(label="📋 Step 1: Profile Setup", id=1) as setup_tab: | |
| 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=5, | |
| value=3, | |
| 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(label="📝 Step 2: Take Assessment", id=2) as assessment_tab: | |
| with gr.Column(visible=True) as question_box: | |
| with gr.Group(): | |
| question_display = gr.Markdown("") | |
| current_options = gr.Radio( | |
| choices=[], | |
| label="Select your answer:", | |
| visible=False | |
| ) | |
| with gr.Row(): | |
| prev_btn = gr.Button("← Previous", variant="secondary", size="sm") | |
| question_counter = gr.Markdown("Question 1 of 3") | |
| next_btn = gr.Button("Next →", variant="secondary", size="sm") | |
| gr.Markdown("---") | |
| 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( | |
| "Back to Assessment", | |
| variant="secondary", | |
| size="lg", | |
| visible=True | |
| ) | |
| # Certification Tab | |
| with gr.Tab(label="🎓 Step 3: Get Certified", id=3) as cert_tab: | |
| 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") | |
| # 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=goto_take_assessment, | |
| 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=goto_certificate, | |
| outputs=tabs | |
| ) | |
| back_to_assessment.click( | |
| fn=goto_take_assessment, | |
| 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() | |