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 | |
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 CertificateGenerator: | |
def __init__(self): | |
self.certificate_size = (1200, 800) | |
self.border_color = '#4682B4' | |
self.background_color = '#F0F8FF' | |
def generate( | |
self, | |
name: str, | |
score: float, | |
course_name: str, | |
company_logo: Optional[str] = None, | |
participant_photo: Optional[str] = None | |
) -> str: | |
certificate = self._create_base_certificate() | |
draw = ImageDraw.Draw(certificate) | |
fonts = self._load_fonts() | |
self._add_borders(draw) | |
self._add_content(draw, fonts, name, score, course_name) | |
self._add_images(certificate, company_logo, participant_photo) | |
return self._save_certificate(certificate) | |
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, | |
score: float, | |
course_name: str | |
): | |
# 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") | |
# Main content | |
content = [ | |
(300, "This is to certify that", 'black'), | |
(380, name.strip() or "Participant", self.border_color), | |
(460, "has successfully completed", 'black'), | |
(540, course_name.strip() or "Assessment", self.border_color), | |
(620, f"with a score of {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() |