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 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""" | |
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}") | |
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} | |
<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") | |
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([]) | |
# UI Components layout | |
gr.Markdown(""" | |
# 🎓 CertifyMe AI | |
### Transform Your Knowledge into Recognized Achievements | |
""") | |
with gr.Tabs() as tabs: | |
# Profile Setup Tab | |
with gr.Tab("📋 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=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("📝 Step 2: Take Assessment"): | |
feedback_box = gr.Markdown("") | |
question_box = gr.Markdown("") | |
answers = [ | |
gr.Radio(choices=[], label=f"Question {i+1}", visible=False) | |
for i in range(5) | |
] | |
submit_btn = gr.Button("Submit Assessment", variant="primary", size="lg") | |
# Certification Tab | |
with gr.Tab("🎓 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") | |
# Event handlers | |
generate_btn.click( | |
fn=quiz_app.update_questions, | |
inputs=[text_input, num_questions], | |
outputs=[ | |
question_box, | |
feedback_box, | |
*answers, | |
current_questions, | |
tabs | |
] | |
) | |
submit_btn.click( | |
fn=quiz_app.submit_quiz, | |
inputs=[*answers, current_questions], | |
outputs=[ | |
feedback_box, | |
*answers, | |
score_display, | |
result_message, | |
tabs | |
] | |
) | |
score_display.change( | |
fn=quiz_app.certificate_generator.generate, | |
inputs=[score_display, name, course_name, company_logo, participant_photo], | |
outputs=certificate_display | |
) | |
return demo | |
if __name__ == "__main__": | |
demo = create_quiz_interface() | |
demo.launch() |